From 63f9f9573dab9745d2f570e1917b325a474fdd93 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 28 Oct 2024 20:07:51 -0400
Subject: reworked LabelBox's to be fully editable.  gor rid of singleLine for
 label boxes. made a text_fitBox field to render text with a labelBox. fixed
 styleprovider in freeformview clusters to be untracked() to avoid
 invalidations.  added a default text color to Settings panel.   fixed
 clicking text buttons to not lose focus in labelBox.

---
 src/client/views/nodes/DocumentView.tsx | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a343b9a39..eb7f333b9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -880,7 +880,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                           background: this.backgroundBoxColor,
                           opacity: this.opacity,
                           cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
-                          color: StrCast(this.layoutDoc.color, 'inherit'),
+                          color: StrCast(this.Document._color, 'inherit'),
                           fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
                           fontSize: Cast(this.Document._text_fontSize, 'string', null),
                           transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
@@ -981,12 +981,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             type: SpringType.GENTLE,
             ...springMappings.gentle,
         };
-        switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
-            case PresEffect.Expand:     return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Expand} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Flip:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Flip}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Rotate:     return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Rotate} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Bounce:     return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Bounce} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Roll}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
+        const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect));
+        switch (presEffect) {
+            case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce:    
+            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
             // case PresEffect.Fade:    return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
             case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>
             // keep as preset, doesn't really make sense with spring config
-- 
cgit v1.2.3-70-g09d2


From 4c768162e0436115a05b9c8b0e4d837d626d45ba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 30 Oct 2024 18:54:52 -0400
Subject: reworked how context menu buttons for ink and text work. added
 disableMixBlend for making transparent docs not use 'multiply'.

---
 src/ClientUtils.ts                                 |  11 +-
 src/client/documents/DocUtils.ts                   |  16 +-
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/CurrentUserUtils.ts                |  78 +++++----
 src/client/util/InteractionUtils.tsx               |   6 +-
 src/client/util/SettingsManager.tsx                |   4 +-
 src/client/util/SnappingManager.ts                 |   7 +
 src/client/views/FilterPanel.tsx                   |   2 +-
 src/client/views/GestureOverlay.tsx                | 164 ++++++------------
 src/client/views/GlobalKeyHandler.ts               |   9 +-
 src/client/views/InkingStroke.tsx                  |   3 +-
 src/client/views/LightboxView.tsx                  |   4 +-
 src/client/views/StyleProp.ts                      |   4 +-
 src/client/views/StyleProvider.tsx                 |   5 +-
 .../views/collections/CollectionDockingView.tsx    |   2 +-
 .../CollectionFreeFormInfoUI.tsx                   |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  71 ++++----
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 src/client/views/global/globalScripts.ts           | 112 +++++++------
 .../views/newlightbox/ButtonMenu/ButtonMenu.tsx    |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   5 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  72 ++++----
 .../views/nodes/FontIconBox/FontIconBox.scss       |   3 +
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 186 ++++++++++++---------
 src/client/views/nodes/ImageBox.tsx                |  10 +-
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/nodes/WebBox.tsx                  |  13 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  22 +--
 src/client/views/pdf/PDFViewer.tsx                 |   2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  12 +-
 src/fields/Doc.ts                                  |   6 +-
 src/fields/InkField.ts                             |  27 +--
 src/pen-gestures/GestureTypes.ts                   |   1 -
 35 files changed, 444 insertions(+), 446 deletions(-)

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

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index e7aee1c2a..baad9a06c 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -107,12 +107,12 @@ export namespace ClientUtils {
             default:                 return type.charAt(0).toUpperCase() + type.substring(1,3);
         } // prettier-ignore
     }
-    export function cleanDocumentType(type: DocumentType, colType: CollectionViewType) {
+    export function cleanDocumentType(type: DocumentType, colType?: CollectionViewType) {
         switch (type) {
             case DocumentType.PDF:   return 'PDF';
             case DocumentType.IMG:   return 'Image';
             case DocumentType.AUDIO: return 'Audio';
-            case DocumentType.COL:   return 'Collection:'+colType;
+            case DocumentType.COL:   return 'Collection:'+ (colType ?? "");
             case DocumentType.RTF:   return 'Text';
             default:                 return type.charAt(0).toUpperCase() + type.slice(1);
         } // prettier-ignore
@@ -212,7 +212,7 @@ export namespace ClientUtils {
         return { r: r, g: g, b: b, a: a };
     }
 
-    const isTransparentFunctionHack = 'isTransparent(__value__)';
+    export const isTransparentFunctionHack = 'isTransparent(__value__)';
     export const noRecursionHack = '__noRecursion';
 
     // special case filters
@@ -223,11 +223,6 @@ export namespace ClientUtils {
     export function IsRecursiveFilter(val: string) {
         return !val.includes(noRecursionHack);
     }
-    export function HasFunctionFilter(val: string) {
-        if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1;
-        // add other function filters here...
-        return undefined;
-    }
 
     export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
         return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index dae3e48ad..067b9c5e0 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -3,7 +3,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { saveAs } from 'file-saver';
 import * as JSZip from 'jszip';
 import { action, runInAction } from 'mobx';
-import { ClientUtils } from '../../ClientUtils';
+import { ClientUtils, DashColor } from '../../ClientUtils';
 import * as JSZipUtils from '../../JSZipUtils';
 import { decycle } from '../../decycler/decycler';
 import { DateField } from '../../fields/DateField';
@@ -36,11 +36,16 @@ import { DocumentView } from '../views/nodes/DocumentView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 
 export namespace DocUtils {
+    function HasFunctionFilter(val: string) {
+        if (val.includes(ClientUtils.isTransparentFunctionHack)) return (d: Doc, color: string) => !d.disableMixBlend && color !== '' && DashColor(color).alpha() !== 1;
+        // add other function filters here...
+        return undefined;
+    }
     function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean {
         let value = valueIn;
-        const hasFunctionFilter = ClientUtils.HasFunctionFilter(value as string);
+        const hasFunctionFilter = HasFunctionFilter(value as string);
         if (hasFunctionFilter) {
-            return hasFunctionFilter(StrCast(doc[key]));
+            return hasFunctionFilter(doc, StrCast(doc[key]));
         }
         if (key === LinkedTo) {
             // links are not a field value, so handled here.  value is an expression of form ([field=]idToDoc("..."))
@@ -683,9 +688,14 @@ export namespace DocUtils {
                       _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
                       _layout_fitWidth: true,
                       _layout_autoHeight: true,
+                      backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
                       text_fitBox: BoolCast(Doc.UserDoc().fitBox),
                       text_align: StrCast(Doc.UserDoc().textAlign),
                       text_fontColor: StrCast(Doc.UserDoc().fontColor),
+                      text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+                      text_fontWeight: StrCast(Doc.UserDoc().fontWeight),
+                      text_fontStyle: StrCast(Doc.UserDoc().fontStyle),
+                      text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration),
                   }),
         });
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b0e1a7545..efd6ce3c9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -927,7 +927,6 @@ export namespace Docs {
             I.text_align = 'center';
             I.rotation = 0;
             I.defaultDoubleClick = 'ignore';
-            I.keepZWhenDragged = true;
             I.author_date = new DateField();
             I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
             // I.acl_Override = SharingPermissions.Unset;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b3aa5e145..98a3e8914 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -4,7 +4,7 @@ import * as rp from 'request-promise';
 import { ClientUtils, OmitKeys } from "../../ClientUtils";
 import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
 import { DocData } from "../../fields/DocSymbols";
-import { InkTool } from "../../fields/InkField";
+import { InkEraserTool, InkInkTool, InkTool } from "../../fields/InkField";
 import { List } from "../../fields/List";
 import { PrefetchProxy } from "../../fields/Proxy";
 import { RichTextField } from "../../fields/RichTextField";
@@ -39,6 +39,7 @@ import { SelectionManager } from "./SelectionManager";
 import { ColorScheme } from "./SettingsManager";
 import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
+import { DocumentView } from "../views/nodes/DocumentView";
 
 export interface Button {
     // DocumentOptions fields a button can set
@@ -731,11 +732,11 @@ pie title Minerals in my tap water
         return [
             { title: "Font",     toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'},
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
-            { title: "Font Size",toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton,              toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
+            { title: " Size",    toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton,              toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
             { title: "Color",    toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
             { title: "Highlight",toolTip: "Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", toolType:"highlight",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
             { title: "Bold",     toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",     ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
-            { title: "Italic",   toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",  ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
+            { title: "Italic",   toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italic",   ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Under",    toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Bullets",  toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",   ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "#",        toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",  ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -758,24 +759,27 @@ pie title Minerals in my tap water
 
     static inkTools():Button[] {
         return [
-            { title: "Pen",      toolTip: "Pen (Ctrl+P)",                   btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                                   scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
-            { title: "Highlight",toolTip: "Highlight (Ctrl+H)",             btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter",                           scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
-            { title: "Write",    toolTip: "Write (Ctrl+Shift+P)",           btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                                 scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
-            { title: "Eraser",   toolTip: "Eraser (Ctrl+E)",                btnType: ButtonType.MultiToggleButton,                toolType: InkTool.Eraser,                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",      toolType: Gestures.Circle,                          scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`,   onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",      toolType: Gestures.Rectangle,                       scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`,   onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",       toolType: Gestures.Line,                            scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`,   onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Ink",    toolTip: "Ink",                              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Ink,                              scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, 
+                subMenu: [
+                    { title: "Pen",      toolTip: "Pen (Ctrl+P)",           btnType: ButtonType.ToggleButton, icon: "pen-nib",     toolType: InkInkTool.Pen,        ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+                    { title: "Highlight",toolTip: "Highlight (Ctrl+H)",     btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight,  ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+                    { title: "Write",    toolTip: "Write (Ctrl+Shift+P)",   btnType: ButtonType.ToggleButton, icon: "pen",         toolType: InkInkTool.Write,      ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },  funcs: {hidden:"IsNoviceMode()" }},      
+                ]},
+            { title: "Width",      toolTip: "Stroke width",                 btnType: ButtonType.NumberSliderButton,                toolType: "strokeWidth",         ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40},
+            { title: "Color",      toolTip: "Stroke color",                 btnType: ButtonType.ColorButton,  icon: "pen",         toolType: "strokeColor",         ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
+            { title: "Eraser",     toolTip: "Eraser (Ctrl+E)",              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Eraser,                           scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
               subMenu: [
-                    { title: "Stroke",   toolTip: "Stroke Erase",           btnType: ButtonType.ToggleButton, icon: "eraser",      toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "xmark",       toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    { title: "Stroke",   toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser",      toolType:InkEraserTool.Stroke,   ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+                    { title: "Segment",  toolTip: "Erase segments up to intersections",btnType: ButtonType.ToggleButton,icon: "xmark",toolType:InkEraserTool.Segment,ignoreClick:true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+                    { title: "Area",     toolTip: "Erase like a pencil",    btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkEraserTool.Radius,   ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
             ]},
-            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}},
-            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType: Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType: Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType: Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
-            { title: "Labels", toolTip: "Labels",                           btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
-            { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
-            { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ 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: " 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()"}},
         ];
     }
 
@@ -814,11 +818,11 @@ pie title Minerals in my tap water
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         expertMode: false,                                                                                                                             width: 30,  scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
             { title: "Header",  icon: "heading",       toolTip: "Doc Titlebar Color",   btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'} },
             { title: "Template",icon: "scroll",        toolTip: "Default Note Template",btnType: ButtonType.ToggleButton,        expertMode: false, toolType:DocumentType.RTF,                                                                                                              scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
-            { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
-            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},       scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
-            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+            { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
+            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},                        scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
             { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
-            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             
             { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
             { title: "Text",    icon: "Text",   toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
@@ -863,7 +867,7 @@ pie title Minerals in my tap water
         }  
         // linear view
         const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
-            childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick,
+            childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick,
             linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
         
         const items = (menutBtn?:Doc) => !menutBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menutBtn) );
@@ -987,15 +991,23 @@ pie title Minerals in my tap water
         Doc.noviceMode ?? (Doc.noviceMode = true);
         doc._showLabel ?? (doc._showLabel = true);
         doc.textAlign ?? (doc.textAlign = "left");
+        doc.textBackgroundColor ?? (doc.textBackgroundColor = Colors.LIGHT_GRAY);
         doc.activeTool = InkTool.None;
         doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
-        doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
-        doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
-        doc.activeInkWidth ?? (doc.activeInkWidth = 1);
-        doc.activeInkBezier ?? (doc.activeInkBezier = "0");
-        doc.activeFillColor ?? (doc.activeFillColor = "");
-        doc.activeArrowStart ?? (doc.activeArrowStart = "");
-        doc.activeArrowEnd ?? (doc.activeArrowEnd = "");
+        doc.activeHideTextLabels ?? (doc.activeHideTextLabels = false);
+        doc[`active${InkInkTool.Pen}Color`]        ?? (doc[`active${InkInkTool.Pen}Color`] = "rgb(0, 0, 0)");
+        doc[`active${InkInkTool.Pen}Width`]        ?? (doc[`active${InkInkTool.Pen}Width`] = 2);
+        doc[`active${InkInkTool.Pen}Bezier`]       ?? (doc[`active${InkInkTool.Pen}Bezier`] = "0");
+        doc[`active${InkInkTool.Write}Color`]      ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)");
+        doc[`active${InkInkTool.Write}Width`]      ?? (doc[`active${InkInkTool.Write}Width`] = 1);
+        doc[`active${InkInkTool.Write}Bezier`]     ?? (doc[`active${InkInkTool.Write}Bezier`] = "0");
+        doc[`active${InkInkTool.Highlight}Color`]  ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent');
+        doc[`active${InkInkTool.Highlight}Width`]  ?? (doc[`active${InkInkTool.Highlight}Width`] = 20);
+        doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0");
+        doc[`active${InkInkTool.Highlight}Fill`]   ?? (doc[`active${InkInkTool.Highlight}Fill`] = "rgba(0, 255, 255, 0.4)");
+        doc.activeInkTool ?? (doc.activeInkTool = InkInkTool.Pen);
+        doc.activeEraserTool ?? (doc.activeEraserTool = InkEraserTool.Stroke);
+        doc.activeEraserWidth ?? (doc.activeEraserWidth = 20);
         doc.activeDash ?? (doc.activeDash === "0");
         doc.fontSize ?? (doc.fontSize = "12px");
         doc.fontFamily ?? (doc.fontFamily = "Arial");
@@ -1130,7 +1142,9 @@ pie title Minerals in my tap water
 }
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser");
+ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser");
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function MySharedDocs()    { return Doc.MySharedDocs; }, "document containing all shared Docs");
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4231c2ca8..ca1cb8014 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -109,7 +109,7 @@ export namespace InteractionUtils {
         dash: string | undefined,
         scalexIn: number,
         scaleyIn: number,
-        shape: Gestures,
+        shape: Gestures | undefined,
         pevents: Property.PointerEvents,
         opacity: number,
         nodefs: boolean,
@@ -136,7 +136,7 @@ export namespace InteractionUtils {
         const arrowLengthFactor = 5 * (markerScale || 0.5);
         const arrowNotchFactor = 2 * (markerScale || 0.5);
         return (
-            <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+            <svg fill={color || 'transparent'} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
                 {' '}
                 {/* setting the svg fill sets the arrowStart fill */}
                 {nodefs ? null : (
@@ -186,7 +186,7 @@ export namespace InteractionUtils {
                         opacity: 1.0,
                         // opacity: strokeWidth !== width ? 0.5 : undefined,
                         pointerEvents: pevents === 'all' ? 'visiblePainted' : pevents,
-                        stroke: color ?? 'rgb(0, 0, 0)',
+                        stroke: (color ?? 'rgb(0, 0, 0)') || 'transparent',
                         strokeWidth,
                         strokeLinecap: strokeLineCap,
                         strokeDasharray: dashArray,
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 33d7d90a8..628097ca8 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -268,9 +268,9 @@ export class SettingsManager extends React.Component<object> {
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={() => {
-                        Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels;
+                        Doc.UserDoc().activeHideTextLabels = !Doc.UserDoc().activeHideTextLabels;
                     }}
-                    toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
+                    toggleStatus={BoolCast(Doc.UserDoc().activeHideTextLabels)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                 />
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 5f6c7d9ac..2a150dc5a 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
 import { observable, action, runInAction, makeObservable } from 'mobx';
+import { Gestures } from '../../pen-gestures/GestureTypes';
 
 export enum freeformScrollMode {
     Pan = 'pan',
@@ -29,6 +30,8 @@ export class SnappingManager {
     @observable _propertyWid: number = 0;
     @observable _printToConsole: boolean = false;
     @observable _hideDecorations: boolean = false;
+    @observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode
+    @observable _inkShape: Gestures | undefined = undefined;
 
     private constructor() {
         SnappingManager._manager = this;
@@ -61,6 +64,8 @@ export class SnappingManager {
     public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
     public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore
     public static get HideDecorations(){ return this.Instance._hideDecorations; } // prettier-ignore
+    public static get KeepGestureMode(){ return this.Instance._keepGestureMode; } // prettier-ignore
+    public static get InkShape()       { return this.Instance._inkShape; } // prettier-ignore
 
     public static SetLongPress      = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
@@ -78,6 +83,8 @@ export class SnappingManager {
     public static SetPropertiesWidth= (wid:number)     =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
     public static SetPrintToConsole = (state:boolean)  =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore
     public static SetHideDecorations= (state:boolean)  =>runInAction(() => {this.Instance._hideDecorations = state}); // prettier-ignore
+    public static SetKeepGestureMode= (state:boolean)  =>runInAction(() => {this.Instance._keepGestureMode = state}); // prettier-ignore
+    public static SetInkShape       = (shape?:Gestures)=>runInAction(() => {this.Instance._inkShape = shape}); // prettier-ignore
 
     public static userColor: string | undefined;
     public static userVariantColor: string | undefined;
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 11425e477..99738052d 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -102,7 +102,7 @@ const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, sel
 
     return (
         <div
-            className={`filterHotKey-button`}
+            className="filterHotKey-button"
             onClick={e => {
                 e.stopPropagation();
                 state.startEditing();
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 59d37df91..2d6cd03e0 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,9 +2,9 @@ import * as fitCurve from 'fit-curve';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction, intersectRect } from '../../Utils';
-import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
 import { Gestures } from '../../pen-gestures/GestureTypes';
@@ -14,27 +14,25 @@ import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
+import { SnappingManager } from '../util/SnappingManager';
 import { undoable } from '../util/UndoManager';
 import './GestureOverlay.scss';
 import { InkingStroke } from './InkingStroke';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { returnEmptyDocViewList } from './StyleProvider';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import {
-    ActiveArrowEnd,
-    ActiveArrowScale,
-    ActiveArrowStart,
-    ActiveDash,
-    ActiveFillColor,
-    ActiveInkBezierApprox,
+    ActiveInkArrowEnd,
+    ActiveInkArrowScale,
+    ActiveInkArrowStart,
     ActiveInkColor,
+    ActiveInkDash,
+    ActiveInkFillColor,
     ActiveInkWidth,
     DocumentView,
-    SetActiveArrowStart,
-    SetActiveDash,
-    SetActiveFillColor,
+    SetActiveInkArrowStart,
     SetActiveInkColor,
+    SetActiveInkDash,
+    SetActiveInkFillColor,
     SetActiveInkWidth,
 } from './nodes/DocumentView';
 export enum ToolglassTools {
@@ -57,18 +55,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     // eslint-disable-next-line no-use-before-define
     static Instances: GestureOverlay[] = [];
 
-    @observable public InkShape: Opt<Gestures> = undefined;
     @observable public SavedColor?: string = undefined;
     @observable public SavedWidth?: number = undefined;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
-    @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode
 
     @observable private _thumbX?: number = undefined;
     @observable private _thumbY?: number = undefined;
     @observable private _pointerY?: number = undefined;
     @observable private _points: { X: number; Y: number }[] = [];
-    @observable private _strokes: InkData[] = [];
-    @observable private _palette?: JSX.Element = undefined;
     @observable private _clipboardDoc?: JSX.Element = undefined;
 
     @computed private get height(): number {
@@ -97,7 +91,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     onPointerDown = (e: React.PointerEvent) => {
         (document.activeElement as HTMLElement)?.blur();
         if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) {
-            if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+            if (Doc.ActiveTool === InkTool.Ink) {
                 this._points.push({ X: e.clientX, Y: e.clientY });
                 setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
             }
@@ -123,13 +117,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     };
 
     @action primCreated() {
-        if (!this.KeepPrimitiveMode) {
-            this.InkShape = undefined;
-            // get out of ink mode after each stroke=
-            // if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+        if (!SnappingManager.KeepGestureMode) {
+            SnappingManager.SetInkShape(undefined);
             Doc.ActiveTool = InkTool.None;
-            // SetActiveArrowStart('none');
-            // SetActiveArrowEnd('none');
         }
     }
     /**
@@ -253,8 +243,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
             const { Name, Score } =
-                (this.InkShape
-                    ? new Result(this.InkShape, 1, Date.now) 
+                (SnappingManager.InkShape
+                    ? new Result(SnappingManager.InkShape, 1, Date.now) 
                     : Doc.UserDoc().recognizeGestures && points.length > 2
                       ? GestureUtils.GestureRecognizer.Recognize([points])
                       : undefined) ?? 
@@ -292,6 +282,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             (downView?.ComponentView as CollectionFreeFormView)?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
             e.preventDefault();
         }
+        this.primCreated();
         this._points.length = 0;
     };
     /**
@@ -557,97 +548,38 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         B.bottom += width / 2;
         B.width += width;
         B.height += width;
-        const fillColor = ActiveFillColor();
+        const fillColor = ActiveInkFillColor();
         const strokeColor = fillColor && fillColor !== 'transparent' ? fillColor : ActiveInkColor();
         return [
             this.props.children,
-            this._palette,
-            [
-                this._strokes.map((l, i) => {
-                    const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
-                    return (
-                        <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
-                            {InteractionUtils.CreatePolyline(
-                                l,
-                                b.left,
-                                b.top,
-                                strokeColor,
-                                width,
-                                width,
-                                'miter',
-                                'round',
-                                ActiveInkBezierApprox(),
-                                'none' /* ActiveFillColor() */,
-                                ActiveArrowStart(),
-                                ActiveArrowEnd(),
-                                ActiveArrowScale(),
-                                ActiveDash(),
-                                1,
-                                1,
-                                this.InkShape as Gestures,
-                                'none',
-                                1.0,
-                                false
-                            )}
-                        </svg>
-                    );
-                }),
-                this._points.length <= 1 ? null : (
-                    <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
-                        {InteractionUtils.CreatePolyline(
-                            this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
-                            B.left,
-                            B.top,
-                            ActiveInkColor(),
-                            width,
-                            width,
-                            'miter',
-                            'round',
-                            '',
-                            'none' /* ActiveFillColor() */,
-                            ActiveArrowStart(),
-                            ActiveArrowEnd(),
-                            ActiveArrowScale(),
-                            ActiveDash(),
-                            1,
-                            1,
-                            this.InkShape as Gestures,
-                            'none',
-                            1.0,
-                            false
-                        )}
-                    </svg>
-                ),
-            ],
+            this._points.length <= 1 ? null : (
+                <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
+                    {InteractionUtils.CreatePolyline(
+                        this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
+                        B.left,
+                        B.top,
+                        strokeColor,
+                        width,
+                        width,
+                        'miter',
+                        'round',
+                        '',
+                        'none' /* ActiveFillColor() */,
+                        ActiveInkArrowStart(),
+                        ActiveInkArrowEnd(),
+                        ActiveInkArrowScale(),
+                        ActiveInkDash(),
+                        1,
+                        1,
+                        SnappingManager.InkShape,
+                        'none',
+                        1.0,
+                        false
+                    )}
+                </svg>
+            ),
         ];
     }
-    screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1);
-    return300 = () => 300;
-    @action
-    public openFloatingDoc = (doc: Doc) => {
-        this._clipboardDoc = (
-            <DocumentView
-                Document={doc}
-                addDocument={undefined}
-                addDocTab={returnFalse}
-                pinToPres={emptyFunction}
-                removeDocument={undefined}
-                ScreenToLocalTransform={this.screenToLocalTransform}
-                PanelWidth={this.return300}
-                PanelHeight={this.return300}
-                isDocumentActive={returnFalse}
-                isContentActive={returnFalse}
-                renderDepth={0}
-                styleProvider={returnEmptyString}
-                containerViewPath={returnEmptyDocViewList}
-                focus={emptyFunction}
-                whenChildContentsActiveChanged={emptyFunction}
-                childFiltersByRanges={returnEmptyFilter}
-                childFilters={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-            />
-        );
-    };
 
     @action
     public closeFloatingDoc = () => {
@@ -694,10 +626,10 @@ ScriptingGlobals.add(function setPen(width: string, color: string, fill: string,
         SetActiveInkColor(color);
         GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
         SetActiveInkWidth(width);
-        SetActiveFillColor(fill);
-        SetActiveArrowStart(arrowStart);
-        SetActiveArrowStart(arrowEnd);
-        SetActiveDash(dash);
+        SetActiveInkFillColor(fill);
+        SetActiveInkArrowStart(arrowStart);
+        SetActiveInkArrowStart(arrowEnd);
+        SetActiveInkDash(dash);
     });
 });
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d7d8e9506..f16338361 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -2,7 +2,7 @@ import { random } from 'lodash';
 import { action } from 'mobx';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { InkTool } from '../../fields/InkField';
+import { InkInkTool, InkTool } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
@@ -280,10 +280,10 @@ export class KeyManager {
                 }
                 break;
             case 'e':
-                Doc.ActiveTool = [InkTool.StrokeEraser, InkTool.SegmentEraser, InkTool.RadiusEraser].includes(Doc.ActiveTool) ? InkTool.None : InkTool.StrokeEraser;
+                Doc.ActiveTool = Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser;
                 break;
             case 'p':
-                Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+                Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
                 break;
             case 'r':
                 preventDefault = false;
@@ -378,7 +378,8 @@ export class KeyManager {
                 UndoManager.Redo();
                 break;
             case 'p':
-                Doc.ActiveTool = InkTool.Write;
+                Doc.ActiveInk = InkInkTool.Write;
+                Doc.ActiveTool = InkTool.Ink;
                 break;
             default:
         }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 270266a94..5199eb02b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -470,14 +470,13 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     className="inkStroke"
                     style={{
                         transform: isInkMask ? `rotate(-${NumCast(this._props.LocalRotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
-                        // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
                     {...interactions}>
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
                 </svg>
-                {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
+                {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeHideTextLabels)) ? null : (
                     <div
                         className="inkStroke-text"
                         style={{
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index a543b4875..3a8a38073 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -214,7 +214,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
     };
     togglePen = () => {
-        Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+        Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
     };
     toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
 
@@ -332,7 +332,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 {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-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
+                {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Ink, 'pen', '', this.togglePen)}
                 {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
             </div>
         );
diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index 44d3bf757..56367e70b 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -19,7 +19,9 @@ export enum StyleProp {
     FontColor = 'fontColor', // color o tet
     FontSize = 'fontSize', // size of text font
     FontFamily = 'fontFamily', // font family of text
-    FontWeight = 'fontWeight', // font weight of text
+    FontWeight = 'fontWeight', // font weight of text (eg bold)
+    FontStyle = 'fontStyle', // font style of text (eg italic)
+    FontDecoration = 'fontDecoration', // text decoration of text (eg underline)
     Highlighting = 'highlighting', // border highlighting
     ContextMenuItems = 'contextMenuItems', // menu items to add to context menu
     AnchorMenuItems = 'anchorMenuItems',
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 12c032964..3a5f57908 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -25,6 +25,7 @@ import { styleProviderQuiz } from './StyleProviderQuiz';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
 import { TagsView } from './TagsView';
+import { InkInkTool } from '../../fields/InkField';
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -171,7 +172,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.FontSize:     return StrCast(doc?.[fieldKey + 'fontSize'],   StrCast(Doc.UserDoc().fontSize));
         case StyleProp.FontFamily:   return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
         case StyleProp.FontWeight:   return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight));
-        case StyleProp.FillColor:    return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
+        case StyleProp.FontStyle:    return StrCast(doc?.[fieldKey + 'fontStyle'], StrCast(Doc.UserDoc().fontStyle));
+        case StyleProp.FontDecoration:return StrCast(doc?.[fieldKey + 'fontDecoration'], StrCast(Doc.UserDoc().fontDecoration));
+        case StyleProp.FillColor:    return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, StrCast(Doc.UserDoc()[Doc.ActiveInk === InkInkTool.Highlight ? "inkHighlighterColor": "inkFillColor"], 'transparent'))));
         case StyleProp.ShowCaption:  return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
         case StyleProp.TitleHeight:  return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
         case StyleProp.ShowTitle:    return (
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e1786d2c9..3e8138390 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView() {
                 }
             }
         }
-        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) {
             e.stopPropagation();
         }
     };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 5d8373fc7..8b9a3e0ec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -29,7 +29,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
     }
     _firstDocPos = { x: 0, y: 0 };
 
-    constructor(props: any) {
+    constructor(props: CollectionFreeFormInfoUIProps) {
         super(props);
         makeObservable(this);
         this._currState = this.setupStates();
@@ -163,7 +163,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                     return presentDocs;
                 }],
                 // eslint-disable-next-line no-use-before-define
-                activePen: [() => activeTool() === InkTool.Pen, () => penMode],
+                activePen: [() => activeTool() === InkTool.Ink, () => penMode],
             },
             'documentation.png',
             () => TopBar.Instance.FlipDocumentationIcon()
@@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
 
         const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', {
             // activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode],
-            activePen: [() => activeTool() !== InkTool.Pen, () => viewedLink],
+            activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink],
         }); // prettier-ignore
 
         // const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ff711314b..f2a5780c5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -10,7 +10,7 @@ import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
+import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
@@ -36,7 +36,20 @@ import { ContextMenu } from '../../ContextMenu';
 import { InkingStroke } from '../../InkingStroke';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView';
+import {
+    ActiveInkArrowEnd,
+    ActiveInkArrowStart,
+    ActiveInkDash,
+    ActiveEraserWidth,
+    ActiveInkFillColor,
+    ActiveInkBezierApprox,
+    ActiveInkColor,
+    ActiveInkWidth,
+    ActiveIsInkMask,
+    DocumentView,
+    SetActiveInkColor,
+    SetActiveInkWidth,
+} from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -497,13 +510,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // prettier-ignore
                 const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                 switch (Doc.ActiveTool) {
-                    case InkTool.Highlighter:
-                    case InkTool.Write:
-                    case InkTool.Pen:
+                    case InkTool.Ink:
                         break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
-                    case InkTool.StrokeEraser:
-                    case InkTool.SegmentEraser:
-                    case InkTool.RadiusEraser:
+                    case InkTool.Eraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
@@ -543,7 +552,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const { points } = ge;
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
                 const inkDoc = this.createInkDoc(points, B);
-                if (Doc.ActiveTool === InkTool.Write) {
+                if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc[DocData].backgroundColor = 'transparent';
+                if (Doc.ActiveInk === InkInkTool.Write) {
                     this.unprocessedDocs.push(inkDoc);
                     CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
                 }
@@ -606,7 +616,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const currPoint = { X: e.clientX, Y: e.clientY };
         this._eraserPts.push([currPoint.X, currPoint.Y]);
         this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
-        if (Doc.ActiveTool === InkTool.RadiusEraser) {
+        if (Doc.ActiveEraser === InkEraserTool.Radius) {
             const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
             strokeMap.forEach((intersects, stroke) => {
                 if (!this._deleteList.includes(stroke)) {
@@ -614,13 +624,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
                     SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
                     const segments = this.radiusErase(stroke, intersects.sort());
-                    segments?.forEach(segment =>
-                        this.forceStrokeGesture(
-                            e,
-                            Gestures.Stroke,
-                            segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
-                        )
-                    );
+                    segments?.forEach(segment => {
+                        const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+                        const bounds = InkField.getBounds(points);
+                        const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+                        const inkDoc = this.createInkDoc(points, B);
+                        ['color', 'fillColor', 'stroke_width', 'stroke_dash', 'stroke_bezier'].forEach(field => {
+                            inkDoc[DocData][field] = stroke.dataDoc[field];
+                        });
+                        this.addDocument(inkDoc);
+                    });
                 }
                 stroke.layoutDoc.opacity = 0;
                 stroke.layoutDoc.dontIntersect = true;
@@ -632,7 +645,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
                     SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
                     // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                    if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+                    if (Doc.ActiveEraser !== InkEraserTool.Stroke) {
                         // this._eraserLock++;
                         const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
                         const newStrokes = segments?.map(segment => {
@@ -1195,14 +1208,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 y: B.y - inkWidth / 2,
                 _width: B.width + inkWidth,
                 _height: B.height + inkWidth,
-                stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
             inkWidth,
             ActiveInkColor(),
             ActiveInkBezierApprox(),
-            ActiveFillColor(),
-            ActiveArrowStart(),
-            ActiveArrowEnd(),
-            ActiveDash(),
+            ActiveInkFillColor(),
+            ActiveInkArrowStart(),
+            ActiveInkArrowEnd(),
+            ActiveInkDash(),
             ActiveIsInkMask()
         );
     };
@@ -1235,14 +1248,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     y: B.y - inkWidth / 2,
                     _width: B.width + inkWidth,
                     _height: B.height + inkWidth,
-                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                    stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
                 inkWidth,
                 opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
-                ActiveArrowStart(),
-                ActiveArrowEnd(),
-                ActiveDash(),
+                stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+                ActiveInkArrowStart(),
+                ActiveInkArrowEnd(),
+                ActiveInkDash(),
                 ActiveIsInkMask()
             );
             this._drawing.push(inkDoc);
@@ -2195,7 +2208,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / this.nativeDimScaling}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
                 }}>
-                {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && (
+                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
                     <div
                         onPointerMove={this.onCursorMove}
                         style={{
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c865c681d..ddc50871d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -690,7 +690,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                 }}
                 style={{
                     overflow: StrCast(this._props.Document._overflow),
-                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
+                    cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
                 }}
                 onDragOver={e => e.preventDefault()}
                 onScroll={e => {
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 013b18a97..40144c4ce 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -3,7 +3,7 @@ import { Colors } from 'browndash-components';
 import { runInAction } from 'mobx';
 import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { InkTool } from '../../../fields/InkField';
+import { InkEraserTool, InkInkTool, InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
@@ -16,21 +16,20 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
 import { InkTranscription } from '../InkTranscription';
-import { InkingStroke } from '../InkingStroke';
 import { PropertiesView } from '../PropertiesView';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import {
     ActiveEraserWidth,
-    ActiveFillColor,
+    ActiveInkFillColor,
     ActiveInkColor,
-    ActiveInkHideTextLabels,
+    ActiveHideTextLabels,
     ActiveInkWidth,
     ActiveIsInkMask,
     DocumentView,
-    SetActiveFillColor,
+    SetActiveInkFillColor,
     SetActiveInkColor,
-    SetActiveInkHideTextLabels,
+    SetactiveHideTextLabels,
     SetActiveInkWidth,
     SetActiveIsInkMask,
     SetEraserWidth,
@@ -60,20 +59,21 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
     const selectedViews = DocumentView.Selected();
+    const selectedDoc = selectedViews.lastElement()?.Document;
+    const defaultFill = selectedDoc?._layout_isSvg ? () => StrCast(selectedDoc[DocData].fillColor) : !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? () => StrCast(Doc.UserDoc().textBackgroundColor, 'transparent') : () => ActiveInkFillColor();
+    const setDefaultFill = !Doc.ActiveTool || Doc.ActiveTool === InkTool.None  ? (c: string) => { Doc.UserDoc().textBackgroundColor = c; }: SetActiveInkFillColor; // prettier-ignore
     if (Doc.ActiveTool !== InkTool.None && !selectedViews.lastElement()?.Document._layout_isSvg) {
-        if (checkResult) {
-            return ActiveFillColor();
-        }
-        SetActiveFillColor(color ?? 'transparent');
+        if (checkResult) return defaultFill();
+        setDefaultFill(color ?? 'transparent');
     } else if (selectedViews.length) {
         if (checkResult) {
             const selView = selectedViews.lastElement();
             const fieldKey = selView.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
             const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
-            return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] ?? 'transparent';
+            return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultFill();
         }
-        selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent');
+        setDefaultFill(color ?? 'transparent');
         selectedViews.forEach(dv => {
             const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
@@ -92,10 +92,13 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
     } else {
         const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
         if (checkResult) {
-            return selected.lastElement()?._backgroundColor ?? 'transparent';
+            return selected.lastElement()?._backgroundColor ?? defaultFill();
         }
-        SetActiveFillColor(color ?? 'transparent');
-        selected.forEach(doc => { doc[DocData].backgroundColor = color; }); // prettier-ignore
+        if (!selected.length) setDefaultFill(color ?? 'transparent');
+        else
+            selected.forEach(doc => {
+                doc[DocData][doc._layout_isSvg ? 'fillColor' : 'backgroundColor'] = color;
+            });
     }
     return '';
 });
@@ -330,7 +333,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     return undefined;
 });
 
-type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italic' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
 type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }];
 
 // eslint-disable-next-line prefer-arrow-callback
@@ -360,10 +363,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
                         toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
         ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold')),
                         toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => { Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; }}],
-        ['italics',   { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics')),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; }}],
-        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline')),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; } }]]
+        ['italic',    { checkResult: () => (editorView ? RichTextMenu.Instance?.italic ?? false : (Doc.UserDoc().fontStyle === 'italic')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleItalic : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italic' ? undefined : 'italic'; }}],
+        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().fontDecoration === 'underline')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().fontDecoration = Doc.UserDoc().fontDecoration === 'underline' ? undefined : 'underline'; } }]]
 
     const map = new Map(attrs.concat(alignments).concat(listings));
     if (checkResult) {
@@ -373,43 +376,46 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     return undefined;
 });
 
-function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
-    const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn;
     if (checkResult) {
-        return ((Doc.ActiveTool === tool || (Doc.UserDoc().activeEraserTool === tool && (tool === toolIn || Doc.ActiveTool === tool))) && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
-            ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
+        return Doc.ActiveTool === tool || Doc.ActiveEraser === tool || Doc.ActiveInk === tool || SnappingManager.InkShape === tool
+            ? true //SnappingManager.KeepGestureMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
             : false;
     }
     runInAction(() => {
+        const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
+        const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
         if (GestureOverlay.Instance) {
-            GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
+            SnappingManager.SetKeepGestureMode(keepPrim);
         }
         if (Object.values(Gestures).includes(tool as Gestures)) {
-            if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
+            if (SnappingManager.InkShape === tool && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
-                GestureOverlay.Instance.InkShape = undefined;
+                SnappingManager.SetInkShape(undefined);
             } else {
-                Doc.ActiveTool = InkTool.Pen;
-                GestureOverlay.Instance.InkShape = tool as Gestures;
+                Doc.ActiveTool = InkTool.Ink;
+                SnappingManager.SetInkShape(tool as Gestures);
             }
-        } else if (tool) {
-            if (Doc.UserDoc().ActiveTool === tool) {
+        } else if (eraserTool) {
+            if (Doc.ActiveTool === InkTool.Eraser && Doc.ActiveTool === tool) {
                 Doc.ActiveTool = InkTool.None;
             } else {
-                if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as InkTool)) {
-                    Doc.UserDoc().activeEraserTool = tool;
-                }
-                // pen or eraser
-                if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
-                    Doc.ActiveTool = InkTool.None;
-                } else {
-                    Doc.ActiveTool = tool as InkTool;
-                    GestureOverlay.Instance.InkShape = undefined;
-                }
+                Doc.ActiveEraser = eraserTool;
+                Doc.ActiveTool = InkTool.Eraser;
+                SnappingManager.SetInkShape(undefined);
+            }
+        } else if (inkTool) {
+            if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveTool === tool) {
+                Doc.ActiveTool = InkTool.None;
+            } else {
+                Doc.ActiveInk = inkTool;
+                Doc.ActiveTool = InkTool.Ink;
+                SnappingManager.SetInkShape(undefined);
             }
         } else {
-            Doc.ActiveTool = InkTool.None;
+            if ((Doc.ActiveTool === tool || !tool) && !keepPrim) Doc.ActiveTool = InkTool.None;
+            else Doc.ActiveTool = tool as InkTool;
         }
     });
     return undefined;
@@ -419,44 +425,44 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function activeEraserTool() {
-    return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
+    return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke);
 }, 'returns the current eraser tool');
 
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: string | number, checkResult?: boolean) {
-    const selected = DocumentView.SelectedDocs().lastElement() ?? Doc.UserDoc();
+    const selected = DocumentView.SelectedDocs().lastElement();
     // prettier-ignore
     const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
             checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
             setInk: (doc: Doc) => { doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask; },
-            setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
+            setMode: () => SetActiveIsInkMask(value ? true : false)
         }],
         ['labels', {
-            checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())),
-            setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = !doc.stroke_showLabel; },
-            setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()),
+            checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_showLabel) : !ActiveHideTextLabels())),
+            setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = value; },
+            setMode: () => SetactiveHideTextLabels(value? false : true),
         }],
         ['fillColor', {
-            checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
+            checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveInkFillColor() ?? "transparent"),
             setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); },
-            setMode: () => SetActiveFillColor(StrCast(value)),
+            setMode: () => SetActiveInkFillColor(StrCast(value)),
         }],
         [ 'strokeWidth', {
             checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
             setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
-            setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+            setMode: () => SetActiveInkWidth(value.toString()),
         }],
         ['strokeColor', {
             checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()),
             setInk: (doc: Doc) => { doc[DocData].color = String(value); },
-            setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK &&  setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+            setMode: () => SetActiveInkColor(StrCast(value))
         }],
         [ 'eraserWidth', {
             checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(),
             setInk: (doc: Doc) => { },
-            setMode: () => { SetEraserWidth(+value);},
+            setMode: () => SetEraserWidth(+value),
         }]
     ]);
 
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index 3eb99f47a..8115bafbf 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-static-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action } from 'mobx';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
@@ -35,10 +33,10 @@ export function ButtonMenu() {
             <div
                 className="newLightboxView-penBtn"
                 title="toggle pen annotation"
-                style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
+                style={{ background: Doc.ActiveTool === InkTool.Ink ? 'white' : undefined }}
                 onClick={e => {
                     e.stopPropagation();
-                    Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+                    Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
                 }}
             />
             <div
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d51b1cd3a..a9edc90e7 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,7 +1,8 @@
+import { Colors } from 'browndash-components';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { OmitKeys } from '../../../ClientUtils';
+import { DashColor, OmitKeys } from '../../../ClientUtils';
 import { numberRange } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { TransitionTimer } from '../../../fields/DocSymbols';
@@ -296,12 +297,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     transition: this.DataTransition(),
                     zIndex: this.ZIndex,
                     display: this.Width ? undefined : 'none',
+                    mixBlendMode: DashColor(StrCast(this.layoutDoc[this.layoutDoc._layout_isSvg ? 'fillColor' : 'backgroundColor'], Colors.WHITE)).alpha() !== 1 ? 'multiply' : undefined,
                 }}>
                 {this.RenderCutoffProvider(this.Document) ? (
                     <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
                         Document={this._props.Document}
                         renderDepth={this._props.renderDepth}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index d79a37181..7925410e2 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -73,7 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />;
     }
     marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
@@ -453,7 +453,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         if ((this.Document._freeform_scale || 1) !== 1) return;
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             this._props.select(false);
             MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
             this._marqueeing = [e.clientX, e.clientY];
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index eb7f333b9..30f9e6363 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -380,7 +380,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
 
     onPointerMove = (e: PointerEvent): void => {
-        if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+        if (e.buttons !== 1 || Doc.ActiveTool === InkTool.Ink) return;
 
         if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
             this.cleanupPointerEvents();
@@ -1560,55 +1560,45 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         } else func();
     }
 }
-
-export function ActiveFillColor(): string { 
-    const dv = DocumentView.Selected().lastElement() ?.Document._layout_isSvg ? DocumentView.Selected().lastElement() : undefined;
-    return StrCast(dv?.Document.fillColor, StrCast(ActiveInkPen()?.activeFillColor, "")); 
-} // prettier-ignore
-export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore
-export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore
-export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore
-export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore
-export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore
-export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore
-export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore
-export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore
-export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore
-export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore
-export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore
-
+export function ActiveHideTextLabels(): boolean { return BoolCast(Doc.UserDoc().activeHideTextLabels, false); } // prettier-ignore
+export function ActiveIsInkMask(): boolean { return BoolCast(Doc.UserDoc()?.activeIsInkMask, false); } // prettier-ignore
+export function ActiveEraserWidth(): number { return Number(Doc.UserDoc()?.activeEraserWidth ?? 25); } // prettier-ignore
+
+export function ActiveInkFillColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Fill`]); } // prettier-ignore
+export function ActiveInkColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Color`], 'black'); } // prettier-ignore
+export function ActiveInkArrowStart(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowStart`], ''); } // prettier-ignore
+export function ActiveInkArrowEnd(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowEnd`], ''); } // prettier-ignore
+export function ActiveInkArrowScale(): number { return NumCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowScale`], 1); } // prettier-ignore
+export function ActiveInkDash(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Dash`], '0'); } // prettier-ignore
+export function ActiveInkWidth(): number { return Number(Doc.UserDoc()?.[`active${Doc.ActiveInk}Width`]); } // prettier-ignore
+export function ActiveInkBezierApprox(): string { return StrCast(Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`]); } // prettier-ignore
+
+export function SetActiveIsInkMask(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeIsInkMask = value); } // prettier-ignore
+export function SetactiveHideTextLabels(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeHideTextLabels = value); } // prettier-ignore
+export function SetEraserWidth(width: number): void { Doc.UserDoc() && (Doc.UserDoc().activeEraserWidth = width); } // prettier-ignore
 export function SetActiveInkWidth(width: string): void {
-    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+    !isNaN(parseInt(width)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Width`] = width);
 }
-export function SetActiveBezierApprox(bezier: string): void {
-    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+export function SetActiveInkBezierApprox(bezier: string): void {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`] = isNaN(parseInt(bezier)) ? '' : bezier);
 }
 export function SetActiveInkColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
-}
-export function SetActiveIsInkMask(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
-}
-export function SetActiveInkHideTextLabels(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
-}
-export function SetActiveFillColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Color`] = value);
 }
-export function SetActiveArrowStart(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+export function SetActiveInkFillColor(value: string) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Fill`] = value);
 }
-export function SetActiveArrowEnd(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+export function SetActiveInkArrowStart(value: string) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowStart`] = value);
 }
-export function SetActiveArrowScale(value: number) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+export function SetActiveInkArrowEnd(value: string) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowEnd`] = value);
 }
-export function SetActiveDash(dash: string): void {
-    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+export function SetActiveInkArrowScale(value: number) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowScale`] = value);
 }
-export function SetEraserWidth(width: number): void {
-    ActiveInkPen() && (ActiveInkPen().eraserWidth = width);
+export function SetActiveInkDash(dash: string): void {
+    !isNaN(parseInt(dash)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}`] = dash);
 }
 
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index ab03a2318..2405889cf 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -13,6 +13,9 @@
 .fonticonbox {
     margin: auto;
     width: 100%;
+    .formLabel {
+        height: 5px;
+    }
 }
 .menuButton {
     height: 100%;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index b45774a75..8c138c2ee 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -4,7 +4,7 @@ import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
@@ -21,6 +21,8 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { OpenWhere } from '../OpenWhere';
 import './FontIconBox.scss';
 import TrailsIcon from './TrailsIcon';
+import { InkTool } from '../../../../fields/InkField';
+import { ScriptField } from '../../../../fields/ScriptField';
 
 export enum ButtonType {
     TextButton = 'textBtn',
@@ -126,7 +128,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 background={SnappingManager.userBackgroundColor}
                 numberDropdownType={type}
                 showPlusMinus={false}
-                tooltip={this.label}
+                formLabel={(StrCast(this.Document.title).startsWith(' ') ? '\u00A0' : '') + StrCast(this.Document.title)}
+                tooltip={StrCast(this.Document.toolTip, this.label)}
                 type={Type.PRIM}
                 min={NumCast(this.dataDoc.numBtnMin, 0)}
                 max={NumCast(this.dataDoc.numBtnMax, 100)}
@@ -148,65 +151,80 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         return false;
     };
 
+    /**
+     * Displays custom dropdown menu for fonts -- this is a HACK -- fix for generality, don't copy
+     */
+    handleFontDropdown = (script: () => string, buttonList: string[]) => {
+        // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+        return {
+            buttonList,
+            jsx: undefined,
+            selectedVal: script(),
+            getStyle: (val: string) => ({ fontFamily: val }),
+        };
+    };
+    /**
+     * Displays custom dropdown menu for view selection -- this is a HACK -- fix for generality, don't copy
+     */
+    handleViewDropdown = (script: ScriptField, buttonList: string[]) => {
+        const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
+        const noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
+        return selected.length === 1 && selected[0].type === DocumentType.COL
+            ? {
+                  buttonList: buttonList.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value as CollectionViewType)),
+                  getStyle: undefined,
+                  selectedVal: StrCast(selected[0]._type_collection),
+              }
+            : {
+                  jsx: selected.length ? (
+                      <Popup
+                          icon={<FontAwesomeIcon size="1x" icon={selected.length > 1 ? 'caret-down' : (Doc.toIcon(selected.lastElement()) as IconProp)} />}
+                          text={selected.length === 1 ? ClientUtils.cleanDocumentType(StrCast(selected[0].type) as DocumentType) : selected.length + ' selected'}
+                          type={Type.TERT}
+                          color={SnappingManager.userColor}
+                          background={SnappingManager.userVariantColor}
+                          popup={<SelectedDocView selectedDocs={selected} />}
+                          fillWidth
+                      />
+                  ) : (
+                      <Button
+                          text={`${Doc.ActiveTool === InkTool.None ? 'Text box' : Doc.ActiveInk} defaults`} //
+                          type={Type.TERT}
+                          color={SnappingManager.userColor}
+                          background={SnappingManager.userVariantColor}
+                          fillWidth
+                          inactive
+                      />
+                  ),
+              };
+    };
+
     /**
      * Dropdown list
      */
     @computed get dropdownListButton() {
         const script = ScriptCast(this.Document.script);
-
-        let noviceList: string[] = [];
-        let text: string | undefined;
-        let getStyle: (val: string) => { [key: string]: string } = () => ({});
-        let icon: IconProp = 'caret-down';
-        const isViewDropdown = script?.script.originalScript.startsWith('{ return setView');
-        if (isViewDropdown) {
-            const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
-            // const selected = DocumentView.SelectedDocs();
-            if (selected.lastElement()) {
-                if (StrCast(selected.lastElement().type) === DocumentType.COL) {
-                    text = StrCast(selected.lastElement()._type_collection);
-                } else {
-                    if (selected.length > 1) {
-                        text = selected.length + ' selected';
-                    } else {
-                        text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType, '' as CollectionViewType);
-                        icon = Doc.toIcon(selected.lastElement());
-                    }
-                    return (
-                        <Popup
-                            icon={<FontAwesomeIcon size="1x" icon={icon} />}
-                            text={text}
-                            type={Type.TERT}
-                            color={SnappingManager.userColor}
-                            background={SnappingManager.userVariantColor}
-                            popup={<SelectedDocView selectedDocs={selected} />}
-                            fillWidth
-                        />
-                    );
-                }
-            } else {
-                return <Button text="None Selected" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} fillWidth inactive />;
-            }
-            noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
-        } else {
-            text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
-            // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
-            if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata
-        }
+        const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
+        const { buttonList, selectedVal, getStyle, jsx } = (() => {
+            switch (this.Document.title) {
+                case 'Font':         return this.handleFontDropdown(selectedFunc, this.buttonList);
+                case 'Perspective':  return this.handleViewDropdown(script, this.buttonList);
+                default:             return { buttonList: this.buttonList, selectedVal: selectedFunc(), jsx: undefined, getStyle: undefined };
+            } // prettier-ignore
+        })();
+        if (jsx) return jsx;
 
         // Get items to place into the list
-        const list: IListItemProps[] = this.buttonList
-            .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
-            .map(value => ({
-                text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
-                val: value,
-                style: getStyle(value),
-                // shortcut: '#',
-            }));
+        const list: IListItemProps[] = buttonList.map(value => ({
+            text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
+            val: value,
+            style: getStyle?.(value),
+            // shortcut: '#',
+        }));
 
         return (
             <Dropdown
-                selectedVal={text}
+                selectedVal={selectedVal}
                 setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)}
                 color={SnappingManager.userColor}
                 background={SnappingManager.userVariantColor}
@@ -215,7 +233,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 dropdownType={DropdownType.SELECT}
                 onItemDown={this.dropdownItemDown}
                 items={list}
-                tooltip={this.label}
+                tooltip={StrCast(this.Document.toolTip, this.label)}
                 fillWidth
             />
         );
@@ -235,51 +253,53 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const tooltip: string = StrCast(this.Document.toolTip);
 
         return (
-            <ColorPicker
-                setSelectedColor={value => {
-                    if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
-                    this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
-                }}
-                setFinalColor={value => {
-                    this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
-                    this.colorBatch?.end();
-                    this.colorBatch = undefined;
-                }}
-                defaultPickerType="Classic"
-                selectedColor={curColor}
-                type={Type.PRIM}
-                color={color}
-                background={SnappingManager.userBackgroundColor}
-                icon={this.Icon(color) ?? undefined}
-                tooltip={tooltip}
-                label={this.label}
-            />
+            <div
+                onPointerDown={e => {
+                    e.stopPropagation();
+                }}>
+                <ColorPicker
+                    setSelectedColor={value => {
+                        if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+                        this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+                    }}
+                    setFinalColor={value => {
+                        this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+                        this.colorBatch?.end();
+                        this.colorBatch = undefined;
+                    }}
+                    defaultPickerType="Classic"
+                    selectedColor={curColor}
+                    type={Type.PRIM}
+                    color={color}
+                    background={SnappingManager.userBackgroundColor}
+                    icon={this.Icon(color) ?? undefined}
+                    tooltip={tooltip}
+                    label={this.label}
+                />
+            </div>
         );
     }
     @computed get multiToggleButton() {
-        // Determine the type of toggle button
-        const tooltip: string = StrCast(this.Document.toolTip);
+        const tooltip = StrCast(this.Document.toolTip);
 
         const script = ScriptCast(this.Document.onClick)?.script;
         const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
 
-        // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
-        const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string;
         const items = DocListCast(this.dataDoc.data);
         const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
 
         return (
             <MultiToggle
-                tooltip={`Toggle ${tooltip}`}
+                tooltip={`Click to Toggle ${tooltip} or select new option`}
                 type={Type.PRIM}
                 color={color}
-                background={background === SnappingManager.userBackgroundColor ? undefined : background}
+                background={undefined}
                 multiSelect={true}
                 onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
                 isToggle={false}
                 toggleStatus={toggleStatus}
-                label={this.label}
+                label={selectedItems.length === 1 ? selectedItems[0] : this.label}
                 items={items.map(item => ({
                     icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
                     tooltip: StrCast(item.toolTip),
@@ -292,7 +312,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                     //   it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define
                     //   arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript)
                     const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val));
-                    itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, itemDoc, _readOnly_: false }));
+                    itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
                 }}
             />
         );
@@ -310,17 +330,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const toggleStatus = (script?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean) ?? false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
-        // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
 
+        // bcz: ink shapes are tri-state - off, one-shot, and on.  Need to update Toggle buttons to allow this and update currentUserUtils to set the tri-state on the Doc
+        // in the meantime, if the button matches a tool type that is not locked, we want to set the background color to something distinct.
+        const inkShapeHack = ((this.Document.toolType && this.Document.toolType === SnappingManager.InkShape) || this.Document.toolType === Doc.ActiveTool) && !SnappingManager.KeepGestureMode;
         return (
             <Toggle
                 tooltip={`Toggle ${tooltip}`}
                 toggleType={ToggleType.BUTTON}
-                type={Type.PRIM}
+                type={inkShapeHack ? Type.TERT : Type.PRIM}
                 toggleStatus={toggleStatus}
                 text={buttonText}
                 color={color}
-                // background={SnappingManager.userBackgroundColor}
+                background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined}
                 icon={this.Icon(color)!}
                 label={this.label}
                 onPointerDown={e =>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0dfc0ec28..ff879a2ab 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -338,7 +338,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);
@@ -465,13 +465,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     marqueeDown = (e: React.PointerEvent) => {
         if (!this.dataDoc[this.fieldKey]) {
             this.chooseImage();
-        } else if (
-            !e.altKey &&
-            e.button === 0 &&
-            NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) &&
-            this._props.isContentActive() &&
-            ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)
-        ) {
+        } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index de51f6447..3bf7de2fe 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -776,7 +776,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // starts marquee selection
     marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a5788d02a..9ba4f8ead 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -44,6 +44,7 @@ import { LinkInfo } from './LinkDocPreview';
 import { OpenWhere } from './OpenWhere';
 import './WebBox.scss';
 
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CreateImage } = require('./WebBoxRenderer');
 
 @observer
@@ -335,7 +336,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 ele = document.createElement('div');
                 ele.append(contents);
             }
-        } catch (e) {
+        } catch {
             /* empty */
         }
         const visibleAnchor = this._getAnchor(this._savedAnnotations, true);
@@ -506,7 +507,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         let href: Opt<string>;
         try {
             href = iframe?.contentWindow?.location.href;
-        } catch (e) {
+        } catch {
             runInAction(() => this._warning++);
             href = undefined;
         }
@@ -713,7 +714,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     this._webUrl = this._url;
                 }
             }
-        } catch (e) {
+        } catch {
             console.log('WebBox URL error:' + this._url);
         }
         return true;
@@ -805,7 +806,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             sel.empty(); // Chrome
         else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
         this.marqueeing = [e.clientX, e.clientY];
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
@@ -855,7 +856,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     })}
                     contentEditable
                     onPointerDown={this.webClipDown}
-                    // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: field.html }}
                 />
             );
@@ -1031,7 +1031,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 {this.inlineTextAnnotations
                     .sort((a, b) => NumCast(a.y) - NumCast(b.y))
                     .map(anno => (
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
                     ))}
             </div>
@@ -1042,7 +1041,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
     renderAnnotations = (childFilters: () => string[]) => (
         <CollectionFreeFormView
-            // eslint-disable-next-line react/jsx-props-no-spreading
             {...this._props}
             setContentViewBox={this.setInnerContent}
             NativeWidth={returnZero}
@@ -1217,7 +1215,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         fieldKey={this.fieldKey + '_' + this._urlHash}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 659cda9dd..905c69bb8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -135,10 +135,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     @observable _showSidebar = false;
 
-    @computed get fontColor() {  return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
-    @computed get fontSize()  {  return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
-    @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
-    @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string;  } // prettier-ignore
+    @computed get fontColor()      { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
+    @computed get fontSize()       { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
+    @computed get fontFamily()     { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
+    @computed get fontWeight()     { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string;  } // prettier-ignore
+    @computed get fontStyle()      { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string;  } // prettier-ignore
+    @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string;  } // prettier-ignore
 
     set _recordingDictation(value) {
         !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
@@ -2119,6 +2121,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     fontSize: this.fontSize,
                     fontFamily: this.fontFamily,
                     fontWeight: this.fontWeight,
+                    fontStyle: this.fontStyle,
+                    textDecoration: this.fontDecoration,
                     ...styleFromLayout,
                 }}>
                 <div
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 33cfbb510..62fbd23ea 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -41,7 +41,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @observable private collapsed: boolean = false;
     @observable private _noLinkActive: boolean = false;
     @observable private _boldActive: boolean = false;
-    @observable private _italicsActive: boolean = false;
+    @observable private _italicActive: boolean = false;
     @observable private _underlineActive: boolean = false;
     @observable private _strikethroughActive: boolean = false;
     @observable private _subscriptActive: boolean = false;
@@ -89,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @computed get underline() {
         return this._underlineActive;
     }
-    @computed get italics() {
-        return this._italicsActive;
+    @computed get italic() {
+        return this._italicActive;
     }
     @computed get strikeThrough() {
         return this._strikethroughActive;
@@ -280,7 +280,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
 
         this._noLinkActive = false;
         this._boldActive = false;
-        this._italicsActive = false;
+        this._italicActive = false;
         this._underlineActive = false;
         this._strikethroughActive = false;
         this._subscriptActive = false;
@@ -290,7 +290,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             switch (mark.name) {
                 case 'noAutoLinkAnchor': this._noLinkActive = true; break;
                 case 'strong':           this._boldActive = true; break;
-                case 'em':               this._italicsActive = true; break;
+                case 'em':               this._italicActive = true; break;
                 case 'underline':        this._underlineActive = true; break;
                 case 'strikethrough':    this._strikethroughActive = true; break;
                 case 'subscript':        this._subscriptActive = true; break;
@@ -352,7 +352,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         }
     };
 
-    toggleItalics = () => {
+    toggleItalic = () => {
         if (this.view) {
             const mark = this.view.state.schema.mark(this.view.state.schema.marks.em);
             this.setMark(mark, this.view.state, this.view.dispatch, false);
@@ -361,13 +361,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     };
 
     setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
-        if (this.dataDoc) {
-            this.dataDoc[`text_${fontField}`] = value;
-            this.updateMenu(undefined, undefined, undefined, this.dataDoc);
-        } else if (this.TextView && this.view) {
+        if (this.TextView && this.view && fontField !== 'fitBox') {
             const { text, paragraph } = this.view.state.schema.nodes;
             const selNode = this.view.state.selection.$anchor.node();
-            if ((fontField === 'fontSize' && value === '0px') || (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type))) {
+            if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
                 this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value;
                 this.view.focus();
             }
@@ -376,6 +373,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
             this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
             this.view.focus();
+        } else if (this.dataDoc) {
+            this.dataDoc[`text_${fontField}`] = value;
+            this.updateMenu(undefined, undefined, undefined, this.dataDoc);
         } else {
             Doc.UserDoc()[fontField] = value;
             this.updateMenu(undefined, undefined, undefined, this.dataDoc);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 358557ad7..920c9ea8b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -377,7 +377,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         if ((e.button !== 0 || e.altKey) && this._props.isContentActive()) {
             this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Document);
         }
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             this._props.select(false);
             MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
             this.isAnnotating = true;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index b4635673c..d0f6566a5 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -21,7 +21,7 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { InkingStroke } from '../InkingStroke';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
 import './SmartDrawHandler.scss';
 
 export interface DrawingOptions {
@@ -105,14 +105,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         y: bounds.top - inkWidth / 2,
                         _width: bounds.width + inkWidth,
                         _height: bounds.height + inkWidth,
-                        stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                        stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
                 inkWidth,
                 opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
-                ActiveArrowStart(),
-                ActiveArrowEnd(),
-                ActiveDash(),
+                stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+                ActiveInkArrowStart(),
+                ActiveInkArrowEnd(),
+                ActiveInkDash(),
                 ActiveIsInkMask()
             );
             drawing.push(inkDoc);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6ec195910..aef7bf330 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -14,7 +14,7 @@ import {
     Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width
 } from './DocSymbols'; // prettier-ignore
 import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
-import { InkTool } from './InkField';
+import { InkEraserTool, InkInkTool, InkTool } from './InkField';
 import { List } from './List';
 import { ObjectField, serverOpType } from './ObjectField';
 import { PrefetchProxy, ProxyField } from './Proxy';
@@ -253,6 +253,10 @@ export class Doc extends RefField {
     public static set ActivePage(val)         { Doc.UserDoc().activePage = val; } // prettier-ignore
     public static get ActiveTool(): InkTool   { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool;  } // prettier-ignore
     public static set ActiveTool(tool:InkTool){ Doc.UserDoc().activeTool = tool; } // prettier-ignore
+    public static get ActiveInk(): InkInkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkInkTool;  } // prettier-ignore
+    public static set ActiveInk(tool:InkInkTool){ Doc.UserDoc().activeInkTool = tool; } // prettier-ignore
+    public static get ActiveEraser(): InkEraserTool { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.None) as InkEraserTool;  } // prettier-ignore
+    public static set ActiveEraser(tool:InkEraserTool){ Doc.UserDoc().activeEraserTool = tool; } // prettier-ignore
     public static get ActivePresentation()    { return DocCast(Doc.ActiveDashboard?.activePresentation) as Opt<Doc>;  } // prettier-ignore
     public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore
     public static get ActiveDashboard()       { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 17b99b033..f3ff5f11f 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -8,17 +8,22 @@ import { ObjectField } from './ObjectField';
 
 // Helps keep track of the current ink tool in use.
 export enum InkTool {
-    None = 'none',
-    Pen = 'pen',
-    Highlighter = 'highlighter',
-    StrokeEraser = 'strokeeraser',
-    SegmentEraser = 'segmenteraser',
-    RadiusEraser = 'radiuseraser',
-    Eraser = 'eraser', // not a real tool, but a class of tools
-    Stamp = 'stamp',
-    Write = 'write',
-    PresentationPin = 'presentationpin',
-    SmartDraw = 'smartdraw',
+    None = 'None',
+    Ink = 'Ink',
+    Eraser = 'Eraser', // not a real tool, but a class of tools
+    SmartDraw = 'Smartdraw',
+}
+
+export enum InkInkTool {
+    Pen = 'Pen',
+    Highlight = 'Highlight',
+    Write = 'Write',
+}
+
+export enum InkEraserTool {
+    Stroke = 'Stroke',
+    Segment = 'Segment',
+    Radius = 'Radius',
 }
 
 export type Segment = Array<Bezier>;
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
index 5a8e9bd97..10f9ba6d0 100644
--- a/src/pen-gestures/GestureTypes.ts
+++ b/src/pen-gestures/GestureTypes.ts
@@ -8,7 +8,6 @@ export enum Gestures {
     Arrow = 'arrow',
     RightAngle = 'rightangle',
 }
-
 // Defines a point in an ink as a pair of x- and y-coordinates.
 export interface PointData {
     X: number;
-- 
cgit v1.2.3-70-g09d2


From 8680d1c31d4f835663c070f5b8cef57254e75e28 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 12 Nov 2024 16:42:35 -0500
Subject: made equation background same as text. fixed dflt stroke/link width
 to both be 1.  made function plot axis ranges get saved to Doc. marked
 equation->function links as being svgs.  fixed initial size of equation
 boxes..

---
 src/client/documents/DocUtils.ts                   |  3 +-
 src/client/util/CurrentUserUtils.ts                |  2 +-
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +---
 src/client/views/global/globalScripts.ts           |  2 +-
 src/client/views/linking/LinkMenuItem.tsx          |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/EquationBox.scss            |  2 -
 src/client/views/nodes/EquationBox.tsx             |  2 +-
 src/client/views/nodes/FunctionPlotBox.tsx         | 43 +++++++++++++++++++---
 src/client/views/nodes/LinkBox.tsx                 |  2 +-
 12 files changed, 50 insertions(+), 22 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 067b9c5e0..741d3a10e 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -168,7 +168,7 @@ export namespace DocUtils {
         return rangeFilteredDocs;
     }
 
-    export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
+    export function MakeLink(source: Doc, target: Doc, linkSettings: { layout_isSvg?: boolean; link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
         if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
         if (target.doc === Doc.UserDoc()) return undefined;
 
@@ -220,6 +220,7 @@ export namespace DocUtils {
                     link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
                     link_relationship: linkSettings.link_relationship,
                     link_description: linkSettings.link_description,
+                    layout_isSvg: linkSettings.layout_isSvg,
                     x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as unknown as number, // x can accept functions even though type says it can't
                     y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as unknown as number, // y can accept functions even though type says it can't
                     link_autoMoveAnchors: true,
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fe23fe062..7b2a7ae0f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1142,7 +1142,7 @@ pie title Minerals in my tap water
 }
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
+ScriptingGlobals.add(function activeInkTool()   { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser");
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 10c2a9898..2bd8bc98a 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -830,7 +830,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set shapeWid(value)  { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
     set shapeHgt(value)  { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
-    @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width); } // prettier-ignore
+    @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width, 1); } // prettier-ignore
     set strokeThk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_width = Math.round(value * 100) / 100; }); } // prettier-ignore
 
     @computed get hgtInput() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3a5f57908..0a9da1237 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -257,7 +257,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 case DocumentType.RTF:         docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break;
                 case DocumentType.LINK:        docColor = (isAnchor ? docColor : undefined); break;
                 case DocumentType.INK:         docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break;
-                case DocumentType.EQUATION:    docColor = docColor || 'transparent'; break;
+                case DocumentType.EQUATION:    docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, 'transparent'); break;
                 case DocumentType.LABEL:       docColor = docColor || Colors.LIGHT_GRAY; break;
                 case DocumentType.BUTTON:      docColor = docColor || Colors.LIGHT_GRAY; break;
                 case DocumentType.IMG:
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f2a5780c5..3de97f836 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -477,11 +477,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             //  do nothing if link is dropped into any freeform view parent of dragged document
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' });
             const added = !!this._props.addDocument?.(source);
-            de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' });
-            if (de.complete.linkDocument) {
-                de.complete.linkDocument.layout_isSvg = true;
-                this.addDocument(de.complete.linkDocument);
-            }
+            de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { layout_isSvg: true, link_relationship: 'annotated by:annotation of' });
+            de.complete.linkDocument && this.addDocument(de.complete.linkDocument);
             e.stopPropagation();
             !added && e.preventDefault();
             return added;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 5a17bc8f6..c632d6001 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -445,7 +445,7 @@ ScriptingGlobals.add(function setInkProperty(option: InkProperty, value: string
             setMode: () => SetactiveHideTextLabels(value? false : true),
         }],
         [ InkProperty.StrokeWidth, {
-            checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
+            checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width, 1) : ActiveInkWidth()),
             setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
             setMode: () => SetActiveInkWidth(value.toString()),
         }],
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index b24fca8e2..1c92c3b0d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -90,7 +90,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
             moveEv => {
                 const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed);
                 dragData.dropPropertiesToRemove = ['hidden'];
-                DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true));
+                DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, () => (this._props.linkDoc._layout_isSvg = true));
                 return true;
             },
             emptyFunction,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 30f9e6363..92e98db87 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -459,10 +459,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 }
                 if (annoData || this.Document !== linkdrag.linkSourceDoc.embedContainer) {
                     const dropDoc = annoData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document;
-                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
+                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {layout_isSvg: true }, undefined, [de.x, de.y - 50]);
                     if (linkDoc) {
                         de.complete.linkDocument = linkDoc;
-                        linkDoc.layout_isSvg = true;
                         DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
                     }
                 }
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index cbd13d924..55e0f5184 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -2,9 +2,7 @@
 
 .equationBox-cont {
     transform-origin: center;
-    background-color: #e7e7e7;
     width: fit-content;
-    min-width: 100%;
     > span {
         width: fit-content;
     }
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 472fa56a0..34b46381d 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -81,7 +81,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 _height: 300,
                 backgroundColor: 'white',
             });
-            const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' });
+            const link = DocUtils.MakeLink(this.Document, graph, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' });
             this._props.addDocument?.(graph);
             link && this._props.addDocument?.(link);
             e.stopPropagation();
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 6b439cd64..91c351895 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -1,5 +1,5 @@
 import functionPlot, { Chart } from 'function-plot';
-import { computed, makeObservable, reaction } from 'mobx';
+import { action, computed, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../fields/Doc';
@@ -15,6 +15,8 @@ import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { FieldView, FieldViewProps } from './FieldView';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 
 @observer
 export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -65,18 +67,24 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         );
         return funcs;
     }
+    computeYScale = (width: number, height: number, xScale: number[]) => {
+        const xDiff = xScale[1] - xScale[0];
+        const yDiff = (height * xDiff) / width;
+        return [-yDiff / 2, yDiff / 2];
+    };
     createGraph = (ele?: HTMLDivElement) => {
         this._plotEle = ele || this._plotEle;
         const width = this._props.PanelWidth();
         const height = this._props.PanelHeight();
+        const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]);
         try {
             this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
             this._plot = functionPlot({
                 target: '#' + this._plotEle?.id,
                 width,
                 height,
-                xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) },
-                yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
+                xAxis: { domain: xrange },
+                yAxis: { domain: this.computeYScale(width, height, xrange) }, //  Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
                 grid: true,
                 data: this.graphFuncs.map(fn => ({
                     fn,
@@ -94,7 +102,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
             const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
                 // const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc);
                 if (res) {
-                    const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' });
+                    const link = DocUtils.MakeLink(doc, this.Document, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' });
                     link && this._props.addDocument?.(link);
                 }
                 return res;
@@ -115,7 +123,32 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
     };
     @computed get theGraph() {
-        return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
+        return (
+            <div
+                id={`${this._plotId}`}
+                ref={r => r && this.createGraph(r)}
+                style={{ position: 'absolute', width: '100%', height: '100%' }}
+                onPointerDown={e => {
+                    e.stopPropagation();
+                    setupMoveUpEvents(
+                        this,
+                        e,
+                        returnFalse,
+                        action(() => {
+                            if (this._plot?.options.xAxis?.domain) {
+                                this.Document.xRange = new List<number>(this._plot.options.xAxis.domain);
+                            }
+                            if (this._plot?.options.yAxis?.domain) {
+                                this.Document.yRange = new List<number>(this._plot.options.yAxis.domain);
+                            }
+                        }),
+                        emptyFunction,
+                        false,
+                        false
+                    );
+                }}
+            />
+        );
     }
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 4d9d2460e..c62a8afb1 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -160,7 +160,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // eslint-disable-next-line camelcase
             const { stroke_markerScale: strokeMarkerScale, stroke_width: strokeRawWidth, stroke_startMarker: strokeStartMarker, stroke_endMarker: strokeEndMarker, stroke_dash: strokeDash } = this.Document;
 
-            const strokeWidth = NumCast(strokeRawWidth, 4);
+            const strokeWidth = NumCast(strokeRawWidth, 1);
             const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
             const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
             return (
-- 
cgit v1.2.3-70-g09d2


From 196b92cb84095780d2b36244831cac03e9b66d8e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 14 Nov 2024 10:48:16 -0500
Subject: changed isScribble related code to be more robust at determine cusps
 and deciding if cusps amount to a scribble. enabled undo of scribble erase
 without undoing scribble. added cusp visualizer for debugging, and cleaned up
 downDocView to  be downFfview on CollectionFreeformView.

---
 src/client/views/GestureOverlay.tsx                | 34 ++++++++++++----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +++++
 src/client/views/nodes/DocumentView.tsx            |  7 +----
 3 files changed, 27 insertions(+), 21 deletions(-)

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

diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index b0a750a9a..777a34ebc 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -64,6 +64,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     @observable private _pointerY?: number = undefined;
     @observable private _points: { X: number; Y: number }[] = [];
     @observable private _clipboardDoc?: JSX.Element = undefined;
+    @observable private _debugCusps: { X: number; Y: number }[] = [];
+    @observable private _debugGestures = false;
 
     @computed private get height(): number {
         return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
@@ -170,9 +172,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
      * Determines if what the array of cusp/intersection data corresponds to a scribble.
      * true if there are at least 4 cusps and either:
      *    1) the initial and final quarters of the array contain objects
-     *    2) or half of the cusps contain objects
+     *    2) or a declining percentage (ranges from 0.5 to 0.2 - based on the number of cusps) of cusp lines intersect strokes
      * @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs
-     * @returns
+     * @returns truthy if it's a scribble
      */
     determineIfScribble = (intersectArray: boolean[]) => {
         const quarterArrayLength = Math.ceil(intersectArray.length / 3.9); // use 3.9 instead of 4 to work better with strokes with only 4 cusps
@@ -182,7 +184,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             }), { start: false, end: false }); // prettier-ignore
 
         const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length;
-        return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end));
+        return intersectArray.length > 3 && (percentCuspsWithContent >= Math.max(0.2, 1 / (intersectArray.length - 1)) || (start && end));
     };
     /**
      * determines if inks intersect
@@ -236,9 +238,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     };
     @action
     onPointerUp = (e: PointerEvent) => {
-        const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
-        const downView = DocumentView.DownDocView;
-        DocumentView.DownDocView = undefined;
+        const ffView = CollectionFreeFormView.DownFfview;
+        CollectionFreeFormView.DownFfview = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
@@ -251,6 +252,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore
 
             const cuspArray = this.getCusps(points);
+            const rect = this._overlayRef.current?.getBoundingClientRect();
+            this._debugCusps = rect ? cuspArray.map(p => ({ X: p.X + B.left - rect?.left, Y: p.Y + B.top - rect.top })) : [];
             // if any of the shape is activated in the CollectionFreeFormViewChrome
             // need to decide when to turn gestures back on
             const actionPerformed = ((name: Gestures) => {
@@ -272,14 +275,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 
             if (!actionPerformed) {
                 const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points);
+                this.dryInk();
                 if (scribbledOver) {
-                    undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')();
-                } else {
-                    this.dryInk();
+                    // can undo the erase without undoing the scribble, or undo a second time to undo the scribble
+                    setTimeout(undoable(() => ffView.removeDocument(scribbledOver.concat([ffView.childDocs.lastElement()])), 'scribble erase'));
                 }
             }
         } else {
-            (downView?.ComponentView as CollectionFreeFormView)?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
+            ffView?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
             e.preventDefault();
         }
         this.primCreated();
@@ -336,13 +339,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     getCusps(points: InkData) {
         const arrayOfPoints: { X: number; Y: number }[] = [];
         arrayOfPoints.push(points[0]);
-        for (let i = 0; i < points.length - 2; i++) {
+        for (let i = 0; i < points.length - 4; i++) {
             const point1 = points[i];
-            const point2 = points[i + 1];
-            const point3 = points[i + 2];
+            const point2 = points[i + 2];
+            const point3 = points[i + 4];
             if (this.find_angle(point1, point2, point3) < 90) {
                 // NOTE: this is not an accurate way to find cusps -- it is highly dependent on sampling rate and doesn't work well with slowly drawn scribbles
                 arrayOfPoints.push(point2);
+                i += 2;
             }
         }
         arrayOfPoints.push(points[points.length - 1]);
@@ -538,7 +542,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
 
     get elements() {
-        const selView = DocumentView.DownDocView;
+        const selView = CollectionFreeFormView.DownFfview;
         const width = Number(ActiveInkWidth()) * NumCast(selView?.Document._freeform_scale, 1); // * (selView?.screenToViewTransform().Scale || 1);
         const rect = this._overlayRef.current?.getBoundingClientRect();
         const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(this._points, true);
@@ -590,7 +594,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         return (
             <div className="gestureOverlay-cont" style={{ pointerEvents: this._props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown}>
                 {this.elements}
-
+                {this._debugGestures && this._debugCusps.map(c => <div key={c.toString()} style={{ top: 0, left: 0, position: 'absolute', transform: `translate(${c.X}px, ${c.Y}px)`, width: 4, height: 4, background: 'red' }} />)}
                 <div
                     className="clipboardDoc-cont"
                     style={{
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3de97f836..49b432d7c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -98,6 +98,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const parent = CollectionFreeFormDocumentView.from(dv)?._props.reactParent;
         return parent instanceof CollectionFreeFormView ? parent : undefined;
     }
+    /**
+     * The Freeformview below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
+     */
+    // eslint-disable-next-line no-use-before-define
+    public static DownFfview: CollectionFreeFormView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
 
     private _clusters = new CollectionFreeFormClusters(this);
     private _oldWheel: HTMLDivElement | null = null;
@@ -497,6 +502,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        if (!CollectionFreeFormView.DownFfview) CollectionFreeFormView.DownFfview = this;
+
         this._downX = this._lastX = e.pageX;
         this._downY = this._lastY = e.pageY;
         this._downTime = Date.now();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 92e98db87..4bfa7fc92 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -355,7 +355,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     onPointerDown = (e: React.PointerEvent): void => {
         if (this._props.isGroupActive?.() === GroupActive.child && !this._props.isDocumentActive?.()) return;
         this._longPressSelector = setTimeout(() => SnappingManager.LongPress && this._props.select(false), 1000);
-        if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView;
 
         this._downX = e.clientX;
         this._downY = e.clientY;
@@ -459,7 +458,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 }
                 if (annoData || this.Document !== linkdrag.linkSourceDoc.embedContainer) {
                     const dropDoc = annoData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document;
-                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {layout_isSvg: true }, undefined, [de.x, de.y - 50]);
+                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, { layout_isSvg: true }, undefined, [de.x, de.y - 50]);
                     if (linkDoc) {
                         de.complete.linkDocument = linkDoc;
                         DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
@@ -1075,10 +1074,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             ?.ComponentView?.updateIcon?.()
             .then(() => ImageCast(DocCast(doc).icon));
     }
-    /**
-     * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
-     */
-    public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
     private _htmlOverlayEffect: Opt<Doc>;
-- 
cgit v1.2.3-70-g09d2


From b7105063030ee29ba644d8fbb5878c8ee41fd389 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 16 Dec 2024 11:52:42 -0500
Subject: fixed api for creating an image using another image as a structure
 reference.

---
 src/client/views/ContextMenuItem.tsx               |  10 +-
 src/client/views/PropertiesView.tsx                |   4 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   3 -
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 src/client/views/nodes/ImageBox.tsx                |   3 +-
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  70 ++++-------
 src/server/ApiManagers/FireflyManager.ts           | 132 ++++++++-------------
 7 files changed, 82 insertions(+), 144 deletions(-)

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

diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 6f8f41bdd..218718b18 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { SnappingManager } from '../util/SnappingManager';
@@ -26,7 +26,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     _hoverTimeout?: NodeJS.Timeout;
     _overPosY = 0;
     _overPosX = 0;
-    @observable _items: ContextMenuProps[] = [];
+    @observable.shallow _items: ContextMenuProps[] = [];
     @observable _overItem = false;
 
     constructor(props: ContextMenuProps & { selected?: boolean }) {
@@ -34,8 +34,8 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
         makeObservable(this);
     }
 
-    componentDidMount() {
-        runInAction(() => this._items.push(...(this._props.subitems ?? [])));
+    @computed get items() {
+        return this._items.concat(this._props.subitems ?? []);
     }
 
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
@@ -91,7 +91,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     };
 
     render() {
-        const submenu = this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
+        const submenu = this.items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
         return this.props.event || this._props.noexpand ? this.renderItem(submenu) : <div className="contextMenu-inlineMenu">{submenu}</div>;
     }
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 4e12e0b2d..aefdeee17 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -995,9 +995,7 @@ 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, 'fill in the details of this image'), 'createImage')}
                             />
                         </div>
                         <div className="color">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 79aad0ef2..272c13546 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -102,7 +102,6 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
             replica: '',
         });
     });
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
@@ -272,7 +271,6 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
         payload: pivotColumnGroups.get(key)?.filters,
     }));
     groupNames.push(...dividers);
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
 }
 
@@ -347,7 +345,6 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
         if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
             groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
         }
-        // eslint-disable-next-line no-use-before-define
         layoutDocsAtTime(keyDocs, key);
     });
     if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4bfa7fc92..8519cda3c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -551,6 +552,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
+            appearanceItems.push({ description: 'Make Image', event: () => DrawingFillHandler.drawingToImage(this.Document, StrCast(this.Document.title)), icon: 'map-pin' });
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -1072,7 +1074,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static GetDocImage(doc: Doc) {
         return DocumentView.getDocumentView(doc)
             ?.ComponentView?.updateIcon?.()
-            .then(() => ImageCast(DocCast(doc).icon));
+            .then(() => ImageCast(doc?.icon, ImageCast(doc[Doc.LayoutFieldKey(doc)])));
     }
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d847b7940..03d417e4e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -40,7 +40,6 @@ import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
-import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -361,6 +360,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    updateIcon = () => new Promise<void>(res => res());
+
     choosePath = (url: URL) => {
         if (!url?.href) return '';
         const lower = url.href.toLowerCase();
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 6f111e95b..32416cec5 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,50 +1,22 @@
-// 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 { Doc } from '../../../fields/Doc';
+import { ImageCast } from '../../../fields/Types';
+import { Upload } from '../../../server/SharedMediaTypes';
+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 = 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('/oldQueryFireflyImage', { prompt, blob, strength });
-//             DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight);
-//             // Networking.PostToServer('/oldQueryFireflyImage', { 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('/oldQueryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
-//         // };
-//     };
-// }
+export class DrawingFillHandler {
+    static drawingToImage = (drawing: Doc, prompt: string) =>
+        DocumentView.GetDocImage(drawing)?.then(imageField => {
+            if (imageField) {
+                const { href } = ImageCast(imageField).url;
+                const hrefParts = href.split('.');
+                const structureUrl = `${hrefParts[0]}_o.${hrefParts[1]}`;
+                const strength: number = 100;
+                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/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 73cf94206..c84e9e8fa 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,21 +20,10 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    askFireflyOld = (prompt: string = 'a realistic illustration of a cat coding', uploadId: string, strength: number) => {
-        const fetched = this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) => {
-                const body: any = {
-                    prompt: prompt,
-                    structure: {
-                        strength: strength,
-                        imageReference: {
-                            source: {
-                                uploadId: uploadId,
-                            },
-                        },
-                    },
-                };
-                return fetch('https://firefly-api.adobe.io/v3/images/generate', {
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', structureUrl: string, strength: number) =>
+        this.getBearerToken().then(response =>
+            response?.json().then((data: { access_token: string }) =>
+                fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
                         ['Content-Type', 'application/json'],
@@ -42,40 +31,40 @@ export default class FireflyManager extends ApiManager {
                         ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
                         ['Authorization', `Bearer ${data.access_token}`],
                     ],
-                    body: JSON.stringify(body),
+                    body: JSON.stringify({
+                        prompt,
+                        structure: !structureUrl
+                            ? undefined
+                            : {
+                                  strength,
+                                  imageReference: {
+                                      source: { url: structureUrl },
+                                  },
+                              },
+                    }),
                 })
-                    .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
+                    .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
                     .catch(error => {
                         console.error('Error:', error);
                         return '';
+                    })
+            )
+        );
+
+    uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
+        new Promise<string>((res, rej) =>
+            fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
+                if (err) {
+                    console.log('Error: ', err);
+                    rej();
+                } else {
+                    dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }).then(response => {
+                        dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => res(link.result.link));
                     });
+                }
             })
         );
-        return fetched;
-    };
 
-    uploadImageToFirefly = (image: Blob) => {
-        const fetched = this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) =>
-                fetch('https://firefly-api.adobe.io/v3/uploads', {
-                    method: 'POST',
-                    headers: [
-                        ['Content-Type', image.type],
-                        ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
-                        ['Authorization', `Bearer ${data.access_token}`],
-                    ],
-                    body: image,
-                })
-                    .then(response => response.json())
-                    .then(data => data.uploadId) // Extract the uploadId from the response
-                    .catch(error => {
-                        console.error('Error:', error);
-                        return '';
-                    })
-            )
-        );
-        return fetched;
-    };
     generateImage = (prompt: string = 'a realistic illustration of a cat coding') => {
         const fetched = this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
@@ -222,17 +211,16 @@ export default class FireflyManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
-            subscription: '/oldQueryFireflyImage',
-            secureHandler: async ({ req, res }) => {
-                const { prompt, imageBlob, strength = 0.5 } = req.body;
-                const uploadId = imageBlob ? await this.uploadImageToFirefly(imageBlob) : null;
-                this.askFireflyOld(prompt, uploadId, strength).then(fire =>
-                    DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                        if (info instanceof Error) _invalid(res, info.message);
-                        else _success(res, info);
-                    })
-                );
-            },
+            subscription: '/queryFireflyImageFromStructure',
+            secureHandler: async ({ req, res }) =>
+                this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
+                    this.generateImageFromStructure(req.body.prompt, uploadUrl, req.body.strength).then(fire =>
+                        DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
+                            if (info instanceof Error) _invalid(res, info.message);
+                            else _success(res, info);
+                        })
+                    )
+                ),
         });
         register({
             method: Method.POST,
@@ -263,36 +251,16 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/expandImage',
             secureHandler: ({ req, res }) =>
-                new Promise<void>((resolve, reject) => {
-                    const dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN });
-                    fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(req.body.file)}`), undefined, (err, contents) => {
-                        if (err) {
-                            console.log('Error: ', err);
-                            reject();
-                        } else {
-                            dbx.filesUpload({ path: `/Apps/browndash/${path.basename(req.body.file)}`, contents })
-                                .then(response => {
-                                    dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => {
-                                        console.log(link.result);
-                                        this.expandImage(link.result.link, req.body.prompt).then(text => {
-                                            if (text.error_code) _error(res, text.message);
-                                            else
-                                                DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
-                                                    if (info instanceof Error) _invalid(res, info.message);
-                                                    else _success(res, info);
-                                                    resolve();
-                                                });
-                                        });
-                                    });
-                                })
-                                .catch(uploadErr => {
-                                    console.log(uploadErr);
-                                    _error(res, 'upload to dropbox failed');
-                                    reject();
-                                });
-                        }
-                    });
-                }),
+                this.uploadImageToDropbox(req.body.file).then(uploadUrl =>
+                    this.expandImage(uploadUrl, req.body.prompt).then(text => {
+                        if (text.error_code) _error(res, text.message);
+                        else
+                            DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
+                                if (info instanceof Error) _invalid(res, info.message);
+                                else _success(res, info);
+                            });
+                    })
+                ),
         });
     }
 }
-- 
cgit v1.2.3-70-g09d2


From 6e7cb570f9bad527cd4772bb5c715dd588fb77df Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 2 Jan 2025 22:39:25 -0500
Subject: tags can now be used as style presets

---
 src/client/apis/gpt/GPT.ts                        |  2 +-
 src/client/views/DocumentButtonBar.tsx            | 18 ++++++++++++++++++
 src/client/views/nodes/DocumentView.tsx           |  2 ++
 src/client/views/smartdraw/DrawingFillHandler.tsx | 10 +++++++---
 src/client/views/smartdraw/FireflyConstants.ts    | 22 ++++++++++++++++++++++
 src/server/ApiManagers/FireflyManager.ts          |  7 +++++--
 6 files changed, 55 insertions(+), 6 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 9241eb120..4b0d58cc1 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -260,7 +260,7 @@ const gptDescribeImage = async (image: string): Promise<string> => {
                     content: [
                         {
                             type: 'text',
-                            text: `Identify what this drawing is, naming as many elements and their location in the drawing as possible`,
+                            text: `Briefly identify what this drawing is, naming all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
                         },
                         {
                             type: 'image_url',
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 32bf67df1..b7033af3f 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -314,6 +314,23 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
+    @computed
+    get aiEditorButton() {
+        const targetDoc = this.view0?.Document;
+        return !targetDoc ? null : (
+            <Tooltip title={<div className="dash-ai-editor-button">Edit with AI</div>}>
+                <div
+                    className="documentButtonBar-icon"
+                    style={{ color: 'white' }}
+                    onClick={() => {
+                        CalendarManager.Instance.open(this.view0, targetDoc);
+                    }}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
+                </div>
+            </Tooltip>
+        );
+    }
+
     @observable _isRecording = false;
     _stopFunc: () => void = emptyFunction;
     @computed
@@ -484,6 +501,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 <div className="documentButtonBar-button">{this.calendarButton}</div>
+                <div className="documentButtonBar-button">{this.aiEditorButton}</div>
                 <div className="documentButtonBar-button">{this.keywordButton}</div>
                 {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 <div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8519cda3c..048b92c71 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1400,6 +1400,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     NativeHeight = () => this.effectiveNativeHeight;
     PanelWidth = () => this.panelWidth;
     PanelHeight = () => this.panelHeight;
+    ReducedPanelWidth = () => this.panelWidth / 2;
+    ReducedPanelHeight = () => this.panelWidth / 2;
     NativeDimScaling = () => this.nativeScaling;
     hideLinkCount = () => !!this.hideLinkButton;
     isHovering = () => this._isHovering;
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 7a95e27c2..52652d377 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -8,10 +8,13 @@ import { Docs } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
-import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants';
+import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
 export class DrawingFillHandler {
-    static drawingToImage = (drawing: Doc, strength: number, prompt: string) =>
+    static drawingToImage = (drawing: Doc, strength: number, prompt: string) => {
+        const docData = drawing[DocData];
+        const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
+        const styles = tags.filter(tag => FireflyStylePresets.has(tag));
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
@@ -31,11 +34,12 @@ export class DrawingFillHandler {
                 imageUrlToBase64(structureUrl)
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then((prompt: string) => {
-                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl, strength }).then((info: Upload.ImageInformation) =>
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl: structureUrl, strength: strength, styles: styles }).then((info: Upload.ImageInformation) =>
                             DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
                         ); // prettier-ignore
                     });
             }
             return false;
         });
+    };
 }
diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts
index f51305fba..3574039e4 100644
--- a/src/client/views/smartdraw/FireflyConstants.ts
+++ b/src/client/views/smartdraw/FireflyConstants.ts
@@ -18,3 +18,25 @@ export const AspectRatioLimits = {
     portrait: 0.875,
     widescreen: 1.472,
 };
+
+// prettier-ignore
+export const FireflyStylePresets = 
+    new Set<string>(['graphic', 'wireframe',
+        'vector_look','bw','cool_colors','golden','monochromatic','muted_color','toned_image','vibrant_colors','warm_tone','closeup',
+        'knolling','landscape_photography','macrophotography','photographed_through_window','shallow_depth_of_field','shot_from_above',
+        'shot_from_below','surface_detail','wide_angle','beautiful','bohemian','chaotic','dais','divine','eclectic','futuristic','kitschy',
+        'nostalgic','simple','antique_photo','bioluminescent','bokeh','color_explosion','dark','faded_image','fisheye','gomori_photography',
+        'grainy_film','iridescent','isometric','misty','neon','otherworldly_depiction','ultraviolet','underwater', 'backlighting', 
+        'dramatic_light', 'golden_hour', 'harsh_light','long','low_lighting','multiexposure','studio_light','surreal_lighting',
+        '3d_patterns','charcoal','claymation','fabric','fur','guilloche_patterns','layered_paper','marble_sculpture','made_of_metal',
+        'origami','paper_mache','polka','strange_patterns','wood_carving','yarn','art_deco','art_nouveau','baroque','bauhaus',
+        'constructivism','cubism','cyberpunk','fantasy','fauvism', 'film_noir','glitch_art','impressionism','industrialism','maximalism',
+        'minimalism','modern_art','modernism','neo','pointillism','psychedelic','science_fiction','steampunk','surrealism','synthetism',
+        'synthwave','vaporwave','acrylic_paint','bold_lines','chiaroscuro','color_shift_art','daguerreotype','digital_fractal',
+        'doodle_drawing','double_exposure_portrait','fresco','geometric_pen','halftone','ink','light_painting','line_drawing','linocut',
+        'oil_paint','paint_spattering','painting','palette_knife','photo_manipulation','scribble_texture','sketch','splattering',
+        'stippling_drawing','watercolor','3d','anime','cartoon','cinematic','comic_book','concept_art','cyber_matrix','digital_art',
+        'flat_design','geometric','glassmorphism','glitch_graphic','graffiti','hyper_realistic','interior_design','line_gradient',
+        'low_poly','newspaper_collage','optical_illusion','pattern_pixel','pixel_art','pop_art','product_photo','psychedelic_background',
+        'psychedelic_wonderland','scandinavian','splash_images','stamp','trompe_loeil'
+    ]);
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 4c4aac5e0..cb5d7c1da 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,9 +20,10 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[]) =>
         this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
+                //prettier-ignore
                 fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
@@ -42,6 +43,8 @@ export default class FireflyManager extends ApiManager {
                                       source: { url: structureUrl },
                                   },
                               },
+                        //prettier-ignore
+                        style: { presets: styles }
                     }),
                 })
                     .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
@@ -227,7 +230,7 @@ export default class FireflyManager extends ApiManager {
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
                 this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
-                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength).then(fire =>
+                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
                         DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
                             if (info instanceof Error) _invalid(res, info.message);
                             else _success(res, info);
-- 
cgit v1.2.3-70-g09d2


From 1d62d867621b293c41ff8488ca5a3bd6010723d5 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 5 Jan 2025 23:47:18 -0500
Subject: added AI image editor

---
 src/client/documents/Documents.ts                  |   6 +-
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/views/DocumentButtonBar.tsx             |   7 +-
 src/client/views/ViewBoxInterface.ts               |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  12 +--
 src/client/views/nodes/DocumentView.tsx            |  22 +++-
 src/client/views/nodes/ImageBox.scss               |  28 +++++
 src/client/views/nodes/ImageBox.tsx                | 113 ++++++++++++++++++++-
 src/client/views/pdf/AnchorMenu.tsx                |  12 +--
 src/client/views/smartdraw/DrawingFillHandler.tsx  |   2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  92 +++++++++--------
 src/client/views/smartdraw/StickerPalette.tsx      |  17 ++--
 src/server/ApiManagers/FireflyManager.ts           |   1 -
 13 files changed, 235 insertions(+), 80 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 785af3409..7f1387ff8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -517,9 +517,9 @@ 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;
+    ai?: string; // to mark items as ai generated
+    ai_firefly_seed?: number;
+    ai_firefly_prompt?: string;
 }
 
 export const DocOptions = new DocumentOptions();
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9af79a02e..b41fd09dc 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: InkProperty.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: InkProperty.Mask,                          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: InkProperty.Labels,                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}},
-            { 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()"}},
+            { title: "Smart Draw", toolTip: "Draw with AI",                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/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index b7033af3f..d722b28b5 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -319,12 +319,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         const targetDoc = this.view0?.Document;
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-ai-editor-button">Edit with AI</div>}>
-                <div
-                    className="documentButtonBar-icon"
-                    style={{ color: 'white' }}
-                    onClick={() => {
-                        CalendarManager.Instance.open(this.view0, targetDoc);
-                    }}>
+                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={undoable(() => this.view0?.toggleAIEditor(), 'toggle AI editor')}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index b7980d74e..df08f2564 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -60,4 +60,5 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
     dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
     isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
+    componentAIView?: (top: number) => JSX.Element;
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ef0b80720..9af698ec7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1290,12 +1290,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const docData = doc[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
         docData._width = opts.size;
-        docData.drawingInput = opts.text;
-        docData.drawingComplexity = opts.complexity;
-        docData.drawingColored = opts.autoColor;
-        docData.drawingSize = opts.size;
-        docData.drawingData = gptRes;
-        docData.ai_generated = true;
+        docData.ai_drawing_input = opts.text;
+        docData.ai_drawing_complexity = opts.complexity;
+        docData.ai_drawing_colored = opts.autoColor;
+        docData.ai_drawing_size = opts.size;
+        docData.ai_drawing_data = gptRes;
+        docData.ai = 'gpt';
         this._drawingContainer = doc;
         this.addDocument(doc);
         this._batch?.end();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 048b92c71..ab58023e3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -123,6 +123,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
+    @observable public _showAIEditor: boolean = false;
+
+    @action
+    showAIEditor() {
+        this._showAIEditor = !this._showAIEditor;
+    }
 
     get _contentDiv()  { return this._mainCont.current; } // prettier-ignore
     get _docView()     { return this._props.DocumentView?.(); } // prettier-ignore
@@ -552,7 +558,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
-            appearanceItems.push({ description: 'Make Image', event: () => DrawingFillHandler.drawingToImage(this.Document, StrCast(this.Document.title)), icon: 'map-pin' });
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -711,6 +716,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
+    rpw = () => this._props.PanelWidth() * 0.6;
+    rph = () => this.panelHeight() * 0.6;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -720,7 +727,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 className="documentView-contentsView"
                 style={{
                     pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                    height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                    width: this._showAIEditor ? this.rpw() : undefined,
+                    height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                    justifySelf: 'center',
                 }}>
                 <DocumentContentsView
                     {...this._props}
@@ -728,7 +737,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     pointerEvents={this.contentPointerEvents}
                     setContentViewBox={this.setContentView}
                     childFilters={this.childFilters}
-                    PanelHeight={this.panelHeight}
+                    PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
+                    PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
                     setHeight={this.setHeight}
                     isContentActive={this.isContentActive}
                     ScreenToLocalTransform={this.screenToLocalContent}
@@ -737,6 +747,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     setTitleFocus={this.setTitleFocus}
                     hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                 />
+                {this._showAIEditor && (this._componentView?.componentAIView?.(this.rph()) ?? null)}
             </div>
         );
     }
@@ -1286,6 +1297,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
     };
 
+    @action
+    public toggleAIEditor = () => {
+        this._docViewInternal && this._docViewInternal.showAIEditor();
+    };
+
     public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
         this._htmlOverlayText = text;
         this._htmlOverlayEffect = effect;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 3ffda5a35..03314e90f 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -139,3 +139,31 @@
 .imageBox-fadeBlocker-hover {
     opacity: 0;
 }
+
+.imageBox-aiView {
+    padding: 5px;
+    position: absolute;
+    overflow: scroll;
+    text-align: center;
+    font-weight: bold;
+    margin-top: 5px;
+
+    .imageBox-aiView-subtitle {
+        align-self: start;
+    }
+
+    .imageBox-aiView-regenerate-container,
+    .imageBox-aiView-options-container {
+        font-weight: normal;
+        text-align: start;
+    }
+
+    .imageBox-aiView-regenerate,
+    .imageBox-aiView-options {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        flex-direction: row;
+        gap: 5px;
+    }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 7ce429f0f..f00580d77 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { Colors } from 'browndash-components';
+import { Colors, Type } from 'browndash-components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
@@ -41,6 +41,9 @@ import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
+import { Button } from 'browndash-components';
+import { SettingsManager } from '../../util/SettingsManager';
+import { AiOutlineSend } from 'react-icons/ai';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -352,7 +355,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }),
                 icon: 'pencil-alt',
             });
-            this.layoutDoc.ai_generated &&
+            this.layoutDoc.ai &&
                 funcs.push({
                     description: 'Regenerate AI Image',
                     event: action(e => {
@@ -526,6 +529,112 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
+    @observable private _regenInput = '';
+    @observable private _canInteract = true;
+    @observable private _regenerateLoading = false;
+
+    componentAIView = (top: number) => {
+        const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+        const showRegenerate = this.Document[DocData].ai;
+        return (
+            <div className="imageBox-aiView" style={{ top: top, width: NumCast(this.Document.width), height: NumCast(this.Document.width) - top }}>
+                Edit Image with AI
+                {showRegenerate && (
+                    <div className="imageBox-aiView-regenerate-container">
+                        <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
+                        <div className="imageBox-aiView-regenerate">
+                            <input
+                                aria-label="Edit instructions input"
+                                // className="smartdraw-input"
+                                type="text"
+                                value={this._regenInput}
+                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                // onKeyDown={this.handleKeyPress}
+                                placeholder="Prompt (Optional)"
+                            />
+                            <Button
+                                text="Regenerate"
+                                type={Type.SEC}
+                                // style={{ alignSelf: 'flex-end' }}
+                                icon={this._regenerateLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                onClick={action(async () => {
+                                    this._regenerateLoading = true;
+                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput);
+                                    this._regenerateLoading = false;
+                                    this._regenInput = '';
+                                })}
+                            />
+                            <Button
+                                // style={{ alignSelf: 'flex-end' }}
+                                text="Get Variations"
+                                type={Type.SEC}
+                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                // onClick={this.handleSendClick}
+                            />
+                        </div>
+                    </div>
+                )}
+                <div className="imageBox-aiView-options-container">
+                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Image Options </text>}
+                    <div className="imageBox-aiView-options">
+                        <Button
+                            type={Type.TERT}
+                            text="Get Text"
+                            icon={<FontAwesomeIcon icon="font" />}
+                            color={SettingsManager.userBackgroundColor}
+                            iconPlacement="right"
+                            onClick={() => {
+                                Networking.PostToServer('/queryFireflyImageText', {
+                                    file: (file => {
+                                        const ext = extname(file);
+                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                                }).then(text => alert(text));
+                            }}
+                        />
+                        <Button
+                            type={Type.TERT}
+                            text="Generative Fill"
+                            icon={<FontAwesomeIcon icon="fill" />}
+                            color={SettingsManager.userBackgroundColor}
+                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={action(() => {
+                                ImageEditorData.Open = true;
+                                ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
+                                ImageEditorData.AddDoc = this._props.addDocument;
+                                ImageEditorData.RootDoc = this.Document;
+                            })}
+                        />
+                        <Button
+                            type={Type.TERT}
+                            text="Expand"
+                            icon={<FontAwesomeIcon icon="expand" />}
+                            color={SettingsManager.userBackgroundColor}
+                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={() => {
+                                Networking.PostToServer('/expandImage', {
+                                    prompt: 'sunny skies',
+                                    file: (file => {
+                                        const ext = extname(file);
+                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                                }).then((info: Upload.ImageInformation) => {
+                                    const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+                                    DocUtils.assignImageInfo(info, img);
+                                    this._props.addDocTab(img, OpenWhere.addRight);
+                                });
+                            }}
+                        />
+                    </div>
+                </div>
+            </div>
+        );
+    };
+
     @computed get annotationLayer() {
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index bb8082061..2e704aa8d 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -153,12 +153,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.AddDrawingAnnotation(drawing);
         const docData = drawing[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
-        docData.drawingInput = opts.text;
-        docData.drawingComplexity = opts.complexity;
-        docData.drawingColored = opts.autoColor;
-        docData.drawingSize = opts.size;
-        docData.drawingData = gptRes;
-        docData.ai_generated = true;
+        docData.ai_drawing_input = opts.text;
+        docData.ai_drawing_complexity = opts.complexity;
+        docData.ai_drawing_colored = opts.autoColor;
+        docData.ai_drawing_size = opts.size;
+        docData.ai_drawing_data = gptRes;
+        docData.ai = 'gpt';
     });
 
     pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 52652d377..8e41ee105 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -35,7 +35,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then((prompt: string) => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl: structureUrl, strength: strength, styles: styles }).then((info: Upload.ImageInformation) =>
-                            DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
+                            DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai: 'firefly', ai_firefly_prompt: prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
                         ); // prettier-ignore
                     });
             }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 6c9470480..0c67c7a13 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -58,7 +58,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
 
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
-    private _selectedDoc: Doc | undefined = undefined;
+    private _selectedDocs: Doc[] = [];
     private _errorOccurredOnce = false;
 
     @observable private _display: boolean = false;
@@ -144,14 +144,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     @action
     displayRegenerate = (x: number, y: number) => {
-        this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
+        this._selectedDocs = [DocumentView.SelectedDocs()?.lastElement()];
         [this._pageX, this._pageY] = [x, y];
         this._display = false;
         this.ShowRegenerate = true;
         this._showEditBox = false;
-        const docData = this._selectedDoc[DocData];
+        const docData = this._selectedDocs[0][DocData];
         this._lastResponse = StrCast(docData.drawingData);
-        this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
+        this._lastInput = { text: StrCast(docData.ai_drawing_input), complexity: NumCast(docData.ai_drawing_complexity), size: NumCast(docData.ai_drawing_size), autoColor: BoolCast(docData.ai_drawing_colored), x: this._pageX, y: this._pageY };
     };
 
     /**
@@ -205,8 +205,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._isLoading = true;
         this._canInteract = false;
         if (this.ShowRegenerate) {
-            await this.regenerate();
+            await this.regenerate(this._selectedDocs);
             runInAction(() => {
+                this._selectedDocs = [];
                 this._regenInput = '';
                 this._showEditBox = false;
             });
@@ -253,7 +254,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 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;
+                drawingDoc && this._selectedDocs.push(drawingDoc);
                 this._errorOccurredOnce = false;
                 return strokeData;
             } else {
@@ -274,57 +275,62 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
                 nativeWidth: dims.width,
                 nativeHeight: dims.height,
-                ai_generated: true,
-                firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
-                firefly_prompt: input,
+                ai: 'firefly',
+                ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
+                ai_firefly_prompt: input,
             });
             DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-            this._selectedDoc = imgDoc;
+            this._selectedDocs.push(imgDoc);
         });
     };
 
     /**
      * Regenerates drawings with the option to add a specific regenerate prompt/request.
+     * @param doc the drawing Docs to regenerate
      */
     @action
-    regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+    regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
         if (lastInput) this._lastInput = lastInput;
         if (lastResponse) this._lastResponse = lastResponse;
         if (regenInput) this._regenInput = regenInput;
-        if (this._generateImage) {
-            if (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);
+        await Promise.all(
+            drawingDocs.map(async doc => {
+                const docData = doc[DocData];
+                if (docData.type == 'image') {
+                    const seed: number = docData?.ai_firefly_seed as number;
+                    if (this._regenInput !== '') {
+                        // if (this._selectedDoc) {
+                        const newPrompt = `${docData.ai_firefly_prompt}, ${this._regenInput}`;
+                        await this.createImageWithFirefly(newPrompt, seed);
+                        // }
+                    } else {
+                        await this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt));
+                    }
                 }
-            } else {
-                await this.createImageWithFirefly(this._lastInput.text);
-            }
-        }
-        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;
+                if (docData.type == 'collection') {
+                    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, doc);
+                        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);
+                    }
                 }
-                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);
-            }
-        }
+            })
+        );
     };
 
     /**
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
index 352a02e32..d23763eb9 100644
--- a/src/client/views/smartdraw/StickerPalette.tsx
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -142,19 +142,20 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
     }
 
     /**
-     * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+     * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from
      * the sticker palette.
      */
     @undoBatch
     generateDrawings = action(async () => {
         this._isLoading = true;
+        const prevDrawings = DocListCast(this._props.Document[DocData].data);
         this._props.Document[DocData].data = undefined;
         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.regenerate(prevDrawings, 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);
             })
         );
@@ -181,12 +182,12 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         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];
-        docData.ai_generated = true;
+        docData.ai_drawing_input = this._opts.text;
+        docData.ai_drawing_complexity = this._opts.complexity;
+        docData.ai_drawing_colored = this._opts.autoColor;
+        docData.ai_drawing_size = this._opts.size;
+        docData.ai_drawing_data = this._gptRes[cIndex];
+        docData.ai = 'gpt';
         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 cb5d7c1da..6daa5840e 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -70,7 +70,6 @@ export default class FireflyManager extends ApiManager {
         );
 
     generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => {
-        console.log('DIMENSIONS', width, height);
         let body = `{  "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`;
         if (seed) {
             console.log('RECEIVED SEED', seed);
-- 
cgit v1.2.3-70-g09d2


From f273c97121300eb9b242f7272cc1cc56c396ca8b Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 6 Jan 2025 18:21:02 -0500
Subject: started a previous images display

---
 src/client/views/ViewBoxInterface.ts            |  3 +-
 src/client/views/nodes/DocumentView.scss        | 17 ++++++-
 src/client/views/nodes/DocumentView.tsx         | 62 +++++++++++++++----------
 src/client/views/nodes/ImageBox.scss            | 28 +++++++++--
 src/client/views/nodes/ImageBox.tsx             | 43 ++++++++++++-----
 src/client/views/smartdraw/SmartDrawHandler.tsx | 35 +++++++-------
 6 files changed, 129 insertions(+), 59 deletions(-)

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

diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index df08f2564..a66a20cf6 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -60,5 +60,6 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
     dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
     isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
-    componentAIView?: (top: number) => JSX.Element;
+    componentAIView?: () => JSX.Element;
+    componentAIViewHistory?: () => JSX.Element;
 }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7568e3b57..9490be98c 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -242,7 +242,7 @@
 
 .contentFittingDocumentView * {
     ::-webkit-scrollbar-track {
-        background: none; 
+        background: none;
     }
 }
 
@@ -270,3 +270,18 @@
         position: relative;
     }
 }
+
+.documentView-editorView-history {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+.documentView-editorView {
+    width: 100%;
+    overflow-y: scroll;
+
+    .documentView-editorView-resizer {
+        height: 5px;
+    }
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ab58023e3..f79eadde8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -723,31 +723,43 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
         const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
-            <div
-                className="documentView-contentsView"
-                style={{
-                    pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                    width: this._showAIEditor ? this.rpw() : undefined,
-                    height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
-                    justifySelf: 'center',
-                }}>
-                <DocumentContentsView
-                    {...this._props}
-                    layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
-                    pointerEvents={this.contentPointerEvents}
-                    setContentViewBox={this.setContentView}
-                    childFilters={this.childFilters}
-                    PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
-                    PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
-                    setHeight={this.setHeight}
-                    isContentActive={this.isContentActive}
-                    ScreenToLocalTransform={this.screenToLocalContent}
-                    rootSelected={this.rootSelected}
-                    onClickScript={this.onClickFunc}
-                    setTitleFocus={this.setTitleFocus}
-                    hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
-                />
-                {this._showAIEditor && (this._componentView?.componentAIView?.(this.rph()) ?? null)}
+            <div>
+                <div
+                    className="documentView-contentsView"
+                    style={{
+                        pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
+                        width: this._showAIEditor ? this.rpw() : undefined,
+                        height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        justifySelf: this._showAIEditor ? 'center' : undefined,
+                    }}>
+                    <DocumentContentsView
+                        {...this._props}
+                        layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
+                        pointerEvents={this.contentPointerEvents}
+                        setContentViewBox={this.setContentView}
+                        childFilters={this.childFilters}
+                        PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
+                        PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
+                        setHeight={this.setHeight}
+                        isContentActive={this.isContentActive}
+                        ScreenToLocalTransform={this.screenToLocalContent}
+                        rootSelected={this.rootSelected}
+                        onClickScript={this.onClickFunc}
+                        setTitleFocus={this.setTitleFocus}
+                        hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
+                    />
+                </div>
+                {this._showAIEditor && (
+                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: this.rpw() / 2 }}>
+                        {this._componentView?.componentAIViewHistory?.() ?? null}
+                    </div>
+                )}
+                {this._showAIEditor && (
+                    <div className="documentView-editorView" style={{ height: this.panelHeight() - this.rph() }}>
+                        <div className="documentView-editorView-resizer" />
+                        {this._componentView?.componentAIView?.() ?? null}
+                    </div>
+                )}
             </div>
         );
     }
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 03314e90f..9f1ac11ef 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -140,15 +140,29 @@
     opacity: 0;
 }
 
-.imageBox-aiView {
+.imageBox-aiView-history {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
     padding: 5px;
-    position: absolute;
-    overflow: scroll;
+    gap: 5px;
+    overflow-y: scroll;
+
+    .imageBox-aiView-img {
+        width: 100%;
+    }
+}
+
+.imageBox-aiView {
+    overflow-y: scroll;
     text-align: center;
     font-weight: bold;
-    margin-top: 5px;
+    align-content: center;
+    height: 100%;
 
     .imageBox-aiView-subtitle {
+        position: relative;
+        left: 5px;
         align-self: start;
     }
 
@@ -156,6 +170,8 @@
     .imageBox-aiView-options-container {
         font-weight: normal;
         text-align: start;
+        margin: 5px;
+        padding-left: 5px;
     }
 
     .imageBox-aiView-regenerate,
@@ -166,4 +182,8 @@
         flex-direction: row;
         gap: 5px;
     }
+
+    .imageBox-aiView-input {
+        width: 50%;
+    }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f00580d77..fc8b7bc27 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -7,8 +7,8 @@ import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { ClientUtils, DashColor, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -34,7 +34,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StickerPalette } from '../smartdraw/StickerPalette';
 import { StyleProp } from '../StyleProp';
-import { DocumentView } from './DocumentView';
+import { DocumentView, DocumentViewInternal } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
@@ -44,6 +44,7 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { Button } from 'browndash-components';
 import { SettingsManager } from '../../util/SettingsManager';
 import { AiOutlineSend } from 'react-icons/ai';
+import { returnEmptyDocViewList } from '../StyleProvider';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -532,18 +533,30 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
+    @observable private _prevImgUrls: string[] = [];
 
-    componentAIView = (top: number) => {
+    componentAIViewHistory = () => {
+        return (
+            <div className="imageBox-aiView-history">
+                {this._prevImgUrls.map((url: string) => (
+                    <img className="imageBox-aiView-img" src={url} onClick={() => (this.dataDoc[this.fieldKey] = new ImageField(url))} />
+                ))}
+            </div>
+        );
+    };
+
+    componentAIView = () => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="imageBox-aiView" style={{ top: top, width: NumCast(this.Document.width), height: NumCast(this.Document.width) - top }}>
+            <div className="imageBox-aiView">
                 Edit Image with AI
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
                         <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
                         <div className="imageBox-aiView-regenerate">
                             <input
+                                className="imageBox-aiView-input"
                                 aria-label="Edit instructions input"
                                 // className="smartdraw-input"
                                 type="text"
@@ -556,14 +569,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 text="Regenerate"
                                 type={Type.SEC}
                                 // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput);
-                                    this._regenerateLoading = false;
-                                    this._regenInput = '';
-                                })}
+                                onClick={undoable(
+                                    action(async () => {
+                                        this._regenerateLoading = true;
+                                        SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDocs => {
+                                            this._prevImgUrls.push(this.paths.lastElement());
+                                            this.dataDoc[this.fieldKey] = new ImageField(newDocs[0]);
+                                            this._regenerateLoading = false;
+                                            this._regenInput = '';
+                                        });
+                                    }),
+                                    'regenerate image'
+                                )}
                             />
                             <Button
                                 // style={{ alignSelf: 'flex-end' }}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 0c67c7a13..13a93367c 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -267,20 +267,23 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     /**
      * Calls Firefly API to create an image based on user input
      */
-    createImageWithFirefly = (input: string, seed?: number) => {
+    createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean) => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
         return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => {
-            const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
-                title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
-                nativeWidth: dims.width,
-                nativeHeight: dims.height,
-                ai: 'firefly',
-                ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
-                ai_firefly_prompt: input,
-            });
-            DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-            this._selectedDocs.push(imgDoc);
+            if (!changeInPlace) {
+                const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                    title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+                    nativeWidth: dims.width,
+                    nativeHeight: dims.height,
+                    ai: 'firefly',
+                    ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
+                    ai_firefly_prompt: input,
+                });
+                DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
+                this._selectedDocs.push(imgDoc);
+            }
+            return img.accessPaths.agnostic.client;
         });
     };
 
@@ -289,22 +292,22 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * @param doc the drawing Docs to regenerate
      */
     @action
-    regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+    regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => {
         if (lastInput) this._lastInput = lastInput;
         if (lastResponse) this._lastResponse = lastResponse;
         if (regenInput) this._regenInput = regenInput;
-        await Promise.all(
+        return await Promise.all(
             drawingDocs.map(async doc => {
                 const docData = doc[DocData];
                 if (docData.type == 'image') {
                     const seed: number = docData?.ai_firefly_seed as number;
                     if (this._regenInput !== '') {
                         // if (this._selectedDoc) {
-                        const newPrompt = `${docData.ai_firefly_prompt}, ${this._regenInput}`;
-                        await this.createImageWithFirefly(newPrompt, seed);
+                        const newPrompt = `${docData.ai_firefly_prompt} ~~~ ${this._regenInput}`;
+                        return this.createImageWithFirefly(newPrompt, seed, changeInPlace);
                         // }
                     } else {
-                        await this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt));
+                        return this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt), undefined, changeInPlace);
                     }
                 }
                 if (docData.type == 'collection') {
-- 
cgit v1.2.3-70-g09d2


From f5ee4974ab41af5b473aa94332ccdd6449db074f Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 7 Jan 2025 20:31:56 -0500
Subject: imagebox now has basic ai functionalities

---
 src/client/views/nodes/DocumentView.scss |  3 ++-
 src/client/views/nodes/DocumentView.tsx  |  2 +-
 src/client/views/nodes/ImageBox.scss     |  8 +++++---
 src/client/views/nodes/ImageBox.tsx      | 21 +++++++++++++++------
 4 files changed, 23 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 9490be98c..82195b9c1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -273,8 +273,9 @@
 
 .documentView-editorView-history {
     position: absolute;
-    left: 0;
+    right: 0;
     top: 0;
+    overflow-y: scroll;
 }
 
 .documentView-editorView {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f79eadde8..622eccc4f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -750,7 +750,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     />
                 </div>
                 {this._showAIEditor && (
-                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: this.rpw() / 2 }}>
+                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) / 2 }}>
                         {this._componentView?.componentAIViewHistory?.() ?? null}
                     </div>
                 )}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 9f1ac11ef..e083f52b4 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -144,17 +144,19 @@
     display: flex;
     flex-direction: column;
     align-items: center;
-    padding: 5px;
+    padding: 5 0 0 5;
     gap: 5px;
-    overflow-y: scroll;
 
     .imageBox-aiView-img {
         width: 100%;
+
+        &:hover {
+            filter: brightness(0.8);
+        }
     }
 }
 
 .imageBox-aiView {
-    overflow-y: scroll;
     text-align: center;
     font-weight: bold;
     align-content: center;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fc8b7bc27..09daea6bb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -533,20 +533,26 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
-    @observable private _prevImgUrls: string[] = [];
+    @observable private _prevImgs: { href: string; pathname: string }[] = [];
 
     componentAIViewHistory = () => {
         return (
             <div className="imageBox-aiView-history">
-                {this._prevImgUrls.map((url: string) => (
-                    <img className="imageBox-aiView-img" src={url} onClick={() => (this.dataDoc[this.fieldKey] = new ImageField(url))} />
+                {this._prevImgs.map(img => (
+                    <img
+                        className="imageBox-aiView-img"
+                        src={img.href}
+                        onClick={() => {
+                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                        }}
+                    />
                 ))}
             </div>
         );
     };
 
     componentAIView = () => {
-        const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+        const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
@@ -575,8 +581,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                     action(async () => {
                                         this._regenerateLoading = true;
                                         SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDocs => {
-                                            this._prevImgUrls.push(this.paths.lastElement());
-                                            this.dataDoc[this.fieldKey] = new ImageField(newDocs[0]);
+                                            const url = newDocs[0];
+                                            const imgField = new ImageField(url);
+                                            this._prevImgs.length === 0 && this._prevImgs.push({ href: this.paths.lastElement(), pathname: field.url.pathname });
+                                            this.dataDoc[this.fieldKey] = imgField;
+                                            this._prevImgs.unshift({ href: this.paths.lastElement(), pathname: url });
                                             this._regenerateLoading = false;
                                             this._regenInput = '';
                                         });
-- 
cgit v1.2.3-70-g09d2


From 51a9f85b4ddc38ac825efcefc0c6db23f3b9914e Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 9 Jan 2025 11:19:31 -0500
Subject: added ai editor to collections

---
 src/client/apis/gpt/GPT.ts                         |  2 +-
 src/client/documents/Documents.ts                  |  1 +
 .../collectionFreeForm/CollectionFreeFormView.scss | 29 +++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 98 +++++++++++++++-------
 src/client/views/nodes/DocumentView.scss           |  2 +
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/ImageBox.scss               |  2 -
 src/client/views/nodes/ImageBox.tsx                | 73 +++++++++-------
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  2 +-
 10 files changed, 151 insertions(+), 64 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b0d58cc1..cf3a28a8e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -260,7 +260,7 @@ const gptDescribeImage = async (image: string): Promise<string> => {
                     content: [
                         {
                             type: 'text',
-                            text: `Briefly identify what this drawing is, naming all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
+                            text: `Very briefly identify what this drawing is and list all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
                         },
                         {
                             type: 'image_url',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7f1387ff8..0bff74ac1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,6 +19,7 @@ import { DocServer } from '../DocServer';
 import { dropActionType } from '../util/DropActionTypes';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
 import { Id } from '../../fields/FieldSymbols';
+import { FireflyImageData } from '../views/smartdraw/FireflyConstants';
 
 class EmptyBox {
     public static LayoutString() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 2c94446fb..dff2cb282 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -304,3 +304,32 @@
         display: none;
     }
 }
+
+.collectionFreeformView-aiView {
+    text-align: center;
+    font-weight: bold;
+    width: 100%;
+
+    .collectionfreeformview-aiView-prompt {
+        height: 25px;
+    }
+
+    .collectionFreeFormView-aiView-strength {
+        text-align: center;
+    }
+
+    .collectionFreeformView-aiView-options-container,
+    .collectionFreeFormView-aiView-regenerate-container {
+        text-align: start;
+        font-weight: normal;
+        padding: 5px;
+    }
+    .collectionFreeformView-aiView-options,
+    .collectionFreeFormView-aiView-regenerate {
+        display: flex;
+        flex-direction: row;
+        gap: 10px;
+        justify-content: center;
+        align-items: center;
+    }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7834eb352..112bfd178 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,9 +1,5 @@
 import { Bezier } from 'bezier-js';
-<<<<<<< HEAD
-import { Button, Colors, Type } from 'browndash-components';
-=======
-import { Colors } from '@dash/components';
->>>>>>> 2f7d1f0073943e1eb9e0f34c4459bc0176377697
+import { Button, Colors, Type } from '@dash/components';
 import { Property } from 'csstype';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -73,6 +69,9 @@ import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import ReactLoading from 'react-loading';
 import { SettingsManager } from '../../../util/SettingsManager';
+import { Slider } from '@mui/material';
+import { AiOutlineSend } from 'react-icons/ai';
+import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -2194,8 +2193,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @observable private _regenInput = '';
+    @observable private _drawingFillInput = '';
     @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
+    @observable private _regenLoading = false;
+    @observable private _drawingFillLoading = false;
+    @observable private _fireflyRefStrength = 50;
+    // _oldDrag: HTMLElement | null = null;
+
+    // onPassiveDrag = (e: PointerEvent) => e.stopPropagation();
+    // protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+    //     // this._dropDisposer?.();
+    //     this._oldDrag?.removeEventListener('pointerdown', this.onPassiveDrag);
+    //     this._oldDrag = ele;
+    //     // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+    //     ele?.addEventListener('pointerdown', this.onPassiveDrag, { passive: false });
+    // };
 
     componentAIViewHistory = () => {
         return (
@@ -2218,49 +2230,79 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     componentAIView = () => {
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="collectionfreeformview-aiView">
-                Edit Image with AI
+            <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
+                Edit Collection with AI
                 {showRegenerate && (
                     <div className="collectionfreeformview-aiView-regenerate-container">
-                        {/* <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
+                        <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
                         <div className="collectionfreeformview-aiView-regenerate">
                             <input
                                 className="collectionfreeformview-aiView-input"
                                 aria-label="Edit instructions input"
-                                // className="smartdraw-input"
                                 type="text"
                                 value={this._regenInput}
                                 onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                // onKeyDown={this.handleKeyPress}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
                                 text="Regenerate"
                                 type={Type.SEC}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDrawings => {
-                                        if (newDrawings[0]) {
-                                        }
-                                    });
+                                    this._regenLoading = true;
+                                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                                    this._regenLoading = false;
                                 })}
                             />
-                            <Button
-                                // style={{ alignSelf: 'flex-end' }}
-                                text="Get Variations"
-                                type={Type.SEC}
-                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                // onClick={this.handleSendClick}
-                            />
-                        </div> */}
+                        </div>
                     </div>
                 )}
                 <div className="collectionfreeformview-aiView-options-container">
-                    {showRegenerate && <text className="collectionfreeformview-aiView-subtitle"> Turn Drawing to Image </text>}
-                    <div className="collectionfreeformview-aiView-options"></div>
+                    <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
+                    <div className="collectionfreeformview-aiView-options">
+                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <div className="collectionfreeformview-aiView-strength">
+                            Reference Strength
+                            <Slider
+                                className="collectionfreeformview-aiView-slider"
+                                sx={{
+                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                }}
+                                min={1}
+                                max={100}
+                                step={1}
+                                size="small"
+                                value={this._fireflyRefStrength}
+                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                valueLabelDisplay="auto"
+                            />
+                        </div>
+                        <Button
+                            text="Send"
+                            type={Type.SEC}
+                            icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={undoable(
+                                action(() => {
+                                    this._drawingFillLoading = true;
+                                    DrawingFillHandler.drawingToImage(
+                                        this.props.Document,
+                                        this._fireflyRefStrength,
+                                        this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
+                                    );
+                                    this._drawingFillInput = '';
+                                    this._drawingFillLoading = false;
+                                }),
+                                'create image'
+                            )}
+                        />
+                    </div>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 82195b9c1..a3d47290a 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -281,6 +281,8 @@
 .documentView-editorView {
     width: 100%;
     overflow-y: scroll;
+    justify-items: center;
+    background-color: rgb(223, 223, 223);
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 622eccc4f..d656bcf50 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -716,8 +716,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    rpw = () => this._props.PanelWidth() * 0.6;
-    rph = () => this.panelHeight() * 0.6;
+    rpw = () => this._props.PanelWidth() / 2;
+    rph = () => this.panelHeight() / 2;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index e083f52b4..9532f4ad3 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -159,8 +159,6 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
-    align-content: center;
-    height: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fe069eacc..db8bb2c6e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { Colors, Button, Type } from '@dash/components';
+import { Colors, Button, Type, Size } from '@dash/components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
@@ -76,6 +76,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     _ffref = React.createRef<CollectionFreeFormView>();
+    _oldWheel: HTMLElement | null = null;
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -94,6 +95,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _error = '';
     @observable private _isHovering = false; // flag to switch between primary and alternate images on hover
 
+    // variables for AI Image Editor
+    @observable private _regenInput = '';
+    @observable private _canInteract = true;
+    @observable private _regenerateLoading = false;
+    @observable private _prevImgs: FireflyImageData[] = [];
+
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
@@ -529,25 +536,34 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
-    @observable private _regenInput = '';
-    @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = [];
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+
+    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+        // this._dropDisposer?.();
+        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+        this._oldWheel = ele;
+        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+    };
 
     componentAIViewHistory = () => {
         return (
-            <div className="imageBox-aiView-history">
+            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
+                <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
                 {this._prevImgs.map(img => (
-                    <img
-                        key={img.pathname}
-                        className="imageBox-aiView-img"
-                        src={img.href}
-                        onClick={() => {
-                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                            this.dataDoc.ai_firefly_prompt = img.prompt;
-                            this.dataDoc.ai_firefly_seed = img.seed;
-                        }}
-                    />
+                    <div>
+                        <img
+                            key={img.pathname}
+                            className="imageBox-aiView-img"
+                            src={img.href}
+                            onClick={() => {
+                                this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                                this.dataDoc.ai_firefly_prompt = img.prompt;
+                                this.dataDoc.ai_firefly_seed = img.seed;
+                            }}
+                        />
+                        <text>{img.prompt}</text>
+                    </div>
                 ))}
             </div>
         );
@@ -557,7 +573,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="imageBox-aiView">
+            <div
+                className="imageBox-aiView"
+                ref={(ele: HTMLDivElement | null) => {
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel = ele;
+                    // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                    ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                }}>
                 Edit Image with AI
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
@@ -566,11 +589,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             <input
                                 className="imageBox-aiView-input"
                                 aria-label="Edit instructions input"
-                                // className="smartdraw-input"
                                 type="text"
                                 value={this._regenInput}
                                 onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                // onKeyDown={this.handleKeyPress}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
@@ -587,22 +608,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                             const imgField = new ImageField(url);
                                             this._prevImgs.length === 0 &&
                                                 this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this.dataDoc[this.fieldKey] = imgField;
                                             this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
+                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
+                                            this.dataDoc[this.fieldKey] = imgField;
                                             this._regenerateLoading = false;
                                             this._regenInput = '';
                                         }
                                     });
                                 })}
                             />
-                            <Button
-                                // style={{ alignSelf: 'flex-end' }}
-                                text="Get Variations"
-                                type={Type.SEC}
-                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                // onClick={this.handleSendClick}
-                            />
+                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
                     </div>
                 )}
@@ -704,8 +719,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] });
         return { width, height };
     };
-
     savedAnnotations = () => this._savedAnnotations;
+
     render() {
         TraceMobx();
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index fea4acb67..43d16df89 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -35,7 +35,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then(prompt => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: user_prompt || prompt, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
                                     { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 9248cbee3..4052ea852 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -279,7 +279,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
                     ai: 'firefly',
-                    ai_firefly_seed: aiseed,
+                    ai_firefly_seed: seed,
                     ai_firefly_prompt: input,
                 });
                 DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-- 
cgit v1.2.3-70-g09d2


From c2312afe4ee969043ba3ce4cb9c275a48b0f1ee0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 10 Jan 2025 12:38:00 -0500
Subject: fixed scrolling when ai editor is open.  rearranged items in ImageBox
 ai editor to be more compact. added error catch handler to queryFireflyImage

---
 src/client/views/nodes/DocumentView.tsx         | 50 +++++++++++++++++--------
 src/client/views/nodes/ImageBox.scss            |  6 +--
 src/client/views/nodes/ImageBox.tsx             | 31 +++------------
 src/client/views/smartdraw/SmartDrawHandler.tsx | 34 +++++++++--------
 4 files changed, 59 insertions(+), 62 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d656bcf50..640d27aa1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,7 +52,6 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
-import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -716,21 +715,21 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    rpw = () => this._props.PanelWidth() / 2;
-    rph = () => this.panelHeight() / 2;
+    @observable _aiWinHeight = 100;
+    rpw = () => this._props.PanelWidth() - this._aiWinHeight;
+    rph = () => this.panelHeight() - this._aiWinHeight;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
         const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
-            <div>
+            <>
                 <div
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
                         width: this._showAIEditor ? this.rpw() : undefined,
                         height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
-                        justifySelf: this._showAIEditor ? 'center' : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -749,20 +748,39 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {this._showAIEditor && (
-                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) / 2 }}>
-                        {this._componentView?.componentAIViewHistory?.() ?? null}
-                    </div>
-                )}
-                {this._showAIEditor && (
-                    <div className="documentView-editorView" style={{ height: this.panelHeight() - this.rph() }}>
-                        <div className="documentView-editorView-resizer" />
-                        {this._componentView?.componentAIView?.() ?? null}
-                    </div>
+                {!this._showAIEditor ? null : (
+                    <>
+                        <div
+                            className="documentView-editorView-history"
+                            ref={r => {
+                                this.historyRef(this._oldAiWheel, (this._oldAiWheel = r));
+                            }}
+                            style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) * 0.95 }}>
+                            {this._componentView?.componentAIViewHistory?.() ?? null}
+                        </div>
+                        <div
+                            className="documentView-editorView"
+                            ref={r => {
+                                this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r));
+                            }}>
+                            <div className="documentView-editorView-resizer" />
+                            {this._componentView?.componentAIView?.() ?? null}
+                        </div>
+                    </>
                 )}
-            </div>
+            </>
         );
     }
+    _oldHistoryWheel: HTMLDivElement | null = null;
+    _oldAiWheel: HTMLDivElement | null = null;
+    onPassiveWheel = (e: WheelEvent) => {
+        e.stopPropagation();
+    };
+
+    protected historyRef = (lastEle: HTMLDivElement | null, ele: HTMLDivElement | null) => {
+        lastEle?.removeEventListener('wheel', this.onPassiveWheel);
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+    };
 
     captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
     fieldsDropdown = (placeholder: string) => (
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 9532f4ad3..1b6b3c85a 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -162,16 +162,14 @@
 
     .imageBox-aiView-subtitle {
         position: relative;
-        left: 5px;
-        align-self: start;
+        align-content: center;
     }
 
     .imageBox-aiView-regenerate-container,
     .imageBox-aiView-options-container {
         font-weight: normal;
-        text-align: start;
         margin: 5px;
-        padding-left: 5px;
+        display: flex;
     }
 
     .imageBox-aiView-regenerate,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index db8bb2c6e..9838af4d0 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -76,7 +76,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     _ffref = React.createRef<CollectionFreeFormView>();
-    _oldWheel: HTMLElement | null = null;
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -536,24 +535,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
-    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
-
-    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
-        // this._dropDisposer?.();
-        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-        this._oldWheel = ele;
-        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
-        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-    };
-
     componentAIViewHistory = () => {
         return (
-            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
+            <div className="imageBox-aiView-history">
                 <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
                 {this._prevImgs.map(img => (
-                    <div>
+                    <div key={img.pathname}>
                         <img
-                            key={img.pathname}
                             className="imageBox-aiView-img"
                             src={img.href}
                             onClick={() => {
@@ -573,18 +561,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div
-                className="imageBox-aiView"
-                ref={(ele: HTMLDivElement | null) => {
-                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                    this._oldWheel = ele;
-                    // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
-                    ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-                }}>
-                Edit Image with AI
+            <div className="imageBox-aiView">
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
-                        <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
                         <div className="imageBox-aiView-regenerate">
                             <input
                                 className="imageBox-aiView-input"
@@ -595,7 +574,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
-                                text="Regenerate"
+                                text="Regenerate Image"
                                 type={Type.SEC}
                                 // style={{ alignSelf: 'flex-end' }}
                                 icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
@@ -622,7 +601,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     </div>
                 )}
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Image Options </text>}
+                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
                     <div className="imageBox-aiView-options">
                         <Button
                             type={Type.TERT}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 4052ea852..f635b5642 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -271,22 +271,24 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
-        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => {
-            const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
-            if (!changeInPlace) {
-                const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
-                    title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
-                    nativeWidth: dims.width,
-                    nativeHeight: dims.height,
-                    ai: 'firefly',
-                    ai_firefly_seed: seed,
-                    ai_firefly_prompt: input,
-                });
-                DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-                this._selectedDocs.push(imgDoc);
-            }
-            return { prompt: input, seed, pathname: img.accessPaths.agnostic.client };
-        });
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed })
+            .then(img => {
+                const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+                if (!changeInPlace) {
+                    const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                        title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+                        nativeWidth: dims.width,
+                        nativeHeight: dims.height,
+                        ai: 'firefly',
+                        ai_firefly_seed: seed,
+                        ai_firefly_prompt: input,
+                    });
+                    DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
+                    this._selectedDocs.push(imgDoc);
+                }
+                return { prompt: input, seed, pathname: img.accessPaths.agnostic.client };
+            })
+            .catch(e => alert('create image failed: ' + e.toString()));
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 12f5a435ee6476e2e07ded0c9cdd597c70ca8af0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 13 Jan 2025 22:26:35 -0500
Subject: changing ai for images and collections to work mostly the same way. 
 fixes for document view/collection/images to keep things working when the ai
 editor view reduces the rendered document size.  fixed so that freeform views
 overlaid on images/vieos/etc have the ui menu items of collections.

---
 src/ClientUtils.ts                                 |  23 +-
 src/client/util/SelectionManager.ts                |   3 +-
 src/client/views/GlobalKeyHandler.ts               |   8 +-
 src/client/views/PropertiesView.tsx                |  48 +++--
 src/client/views/TagsView.tsx                      |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  15 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 236 ++++++++++-----------
 src/client/views/nodes/DocumentView.scss           |   1 -
 src/client/views/nodes/DocumentView.tsx            |  48 +++--
 src/client/views/nodes/ImageBox.scss               |  19 +-
 src/client/views/nodes/ImageBox.tsx                | 113 ++++++----
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  17 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   8 +-
 src/server/ApiManagers/UploadManager.ts            |  22 +-
 14 files changed, 316 insertions(+), 247 deletions(-)

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

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 8f62b9060..e5dbb81db 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -144,15 +144,20 @@ export namespace ClientUtils {
     export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) {
         try {
             const posting = ClientUtils.prepend('/uploadURI');
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename,
-                },
-                json: true,
-            });
+            const returnedUri = await rp
+                .post(posting, {
+                    body: {
+                        uri: imageUri,
+                        name: returnedFilename,
+                        nosuffix,
+                        replaceRootFilename,
+                    },
+                    json: true,
+                })
+                .catch(e => {
+                    alert('Data URI Error: ' + e.toString());
+                    return undefined;
+                });
             return returnedUri;
         } catch (e) {
             console.log('ConvertDataURI :' + e);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1ab84421c..a1f2849cd 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -88,7 +88,8 @@ ScriptingGlobals.add(function SelectedDocType(type: string, expertMode: boolean,
         return DocumentView.Selected().lastElement()?._props.renderDepth === 0;
     }
     const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(DocumentView.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
-    return selected?.type === type || selected?.type_collection === type || !type;
+    const matchOverlayFreeform = type === CollectionViewType.Freeform && DocumentView.Selected().lastElement()?.ComponentView?.annotationKey;
+    return matchOverlayFreeform || selected?.type === type || selected?.type_collection === type || !type;
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deselectAll() {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f16338361..b200aff65 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -169,10 +169,10 @@ export class KeyManager {
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
-            case 'arrowleft':  return this.nudge(-1,0, 'nudge left')
-            case 'arrowright': return this.nudge(1,0, 'nudge right');
-            case 'arrowup':    return this.nudge(0, -1, 'nudge up');
-            case 'arrowdown':  return this.nudge(0, 1, 'nudge down');
+            case 'arrowleft':  return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself.
+            case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right');
+            case 'arrowup':    return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up');
+            case 'arrowdown':  return (e.target as any).type !== 'text' && this.nudge(0,  1, 'nudge down');
             default:
         } // prettier-ignore
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e52189f56..42aa6782f 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -111,6 +111,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable openContexts: boolean = true;
     @observable openLinks: boolean = true;
     @observable openAppearance: boolean = true;
+    @observable openFirefly: boolean = true;
     @observable openTransform: boolean = true;
     @observable openFilters: boolean = false;
     @observable openStyling: boolean = true;
@@ -982,26 +983,10 @@ 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 && (
-                    <div>
-                        <div className="drawing-to-image">
-                            <Toggle
-                                text={'Create Image'}
-                                color={SettingsManager.userColor}
-                                icon={<FontAwesomeIcon icon="fill-drip" />}
-                                iconPlacement="left"
-                                align="flex-start"
-                                fillWidth
-                                toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
-                            />
-                        </div>
-                        <div className="strength-slider">{strength}</div>
+                <div>
+                    {!targetDoc.layout_isSvg && this.containsInkDoc && (
                         <div className="color">
                             <Toggle
                                 text={'Color with GPT'}
@@ -1016,8 +1001,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 }, 'colorWithGPT')}
                             />
                         </div>
-                    </div>
-                )}
+                    )}
+                </div>
                 <div className="smooth">
                     <Toggle
                         text={'Smooth Ink Strokes'}
@@ -1113,6 +1098,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         this.openTransform = false;
         this.openFields = false;
         this.openSharing = false;
+        this.openAppearance = false;
+        this.openFirefly = false;
         this.openLayout = false;
         this.openFilters = false;
     };
@@ -1323,11 +1310,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get inkSubMenu() {
+        const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
+            !isNaN(val) && (this.refStrength = val);
+        });
+        const targetDoc = this.selectedLayoutDoc;
         return (
             <>
                 <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
                     {this.selectedStrokes.length ? this.inkEditor : null}
                 </PropertiesSection>
+                <PropertiesSection  title="Firefly" isOpen={this.openFirefly} setIsOpen={bool => { this.openFirefly = bool; }} onDoubleClick={this.CloseAll}>
+                    <>
+                <div className="drawing-to-image">
+                        <Toggle
+                            text="Create Image"
+                            color={SettingsManager.userColor}
+                            icon={<FontAwesomeIcon icon="fill-drip" />}
+                            iconPlacement="left"
+                            align="flex-start"
+                            fillWidth
+                            toggleType={ToggleType.BUTTON}
+                            onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
+                        />
+                    </div>
+                    <div className="strength-slider">{strength}</div>
+                    </>
+                </PropertiesSection>
                 <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}>
                     {this.transformEditor}
                 </PropertiesSection>
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 21b6a76c7..0edd89204 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -366,7 +366,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                     backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
                     borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
                     position: 'relative',
-                    top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+                    top: this._props.Views.lastElement()._showAIEditor ? undefined : this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
                 }}>
                 <div className="tagsView-content" style={{ width: '100%' }}>
                     <div className="tagsView-list">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index dff2cb282..82887a7a5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -312,10 +312,14 @@
 
     .collectionfreeformview-aiView-prompt {
         height: 25px;
+        width: 100%;
     }
 
     .collectionFreeFormView-aiView-strength {
         text-align: center;
+        align-items: center;
+        display: flex;
+        width: 150px;
     }
 
     .collectionFreeformView-aiView-options-container,
@@ -323,13 +327,20 @@
         text-align: start;
         font-weight: normal;
         padding: 5px;
+        width: 100%;
+        display: flex;
+        .collectionFreeformView-aiView-subtitle {
+            margin: auto;
+            width: 40px;
+        }
     }
     .collectionFreeformView-aiView-options,
     .collectionFreeFormView-aiView-regenerate {
         display: flex;
         flex-direction: row;
-        gap: 10px;
-        justify-content: center;
         align-items: center;
+        align-items: center;
+        width: 100%;
+        gap: 10px;
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 112bfd178..5b4f81379 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1877,8 +1877,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             : UpdateIcon(
                   this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
                   contentDiv,
-                  usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                  usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+                  usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+                  usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
                   this._props.PanelWidth(),
                   this._props.PanelHeight(),
                   0,
@@ -2228,45 +2228,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     componentAIView = () => {
-        const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
-                Edit Collection with AI
-                {showRegenerate && (
-                    <div className="collectionfreeformview-aiView-regenerate-container">
-                        <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
-                        <div className="collectionfreeformview-aiView-regenerate">
-                            <input
-                                className="collectionfreeformview-aiView-input"
-                                aria-label="Edit instructions input"
-                                type="text"
-                                value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                placeholder="Prompt (Optional)"
-                            />
-                            <Button
-                                text="Regenerate"
-                                type={Type.SEC}
-                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenLoading = true;
-                                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
-                                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
-                                    this._regenLoading = false;
-                                })}
-                            />
-                        </div>
-                    </div>
-                )}
                 <div className="collectionfreeformview-aiView-options-container">
-                    <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
+                    <span className="collectionfreeformview-aiView-subtitle">Firefly:</span>
                     <div className="collectionfreeformview-aiView-options">
-                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <input
+                            className="collectionfreeformview-aiView-prompt"
+                            placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
+                            type="text"
+                            value={this._drawingFillInput}
+                            onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))}
+                        />
                         <div className="collectionfreeformview-aiView-strength">
-                            Reference Strength
+                            Similarity
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
                                 sx={{
@@ -2286,24 +2261,49 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         <Button
                             text="Send"
                             type={Type.SEC}
-                            icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
                             onClick={undoable(
                                 action(() => {
                                     this._drawingFillLoading = true;
-                                    DrawingFillHandler.drawingToImage(
-                                        this.props.Document,
-                                        this._fireflyRefStrength,
-                                        this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
+                                        action(() => {
+                                            this._drawingFillLoading = false;
+                                        })
                                     );
-                                    this._drawingFillInput = '';
-                                    this._drawingFillLoading = false;
                                 }),
                                 'create image'
                             )}
                         />
                     </div>
                 </div>
+                <div className="collectionfreeformview-aiView-regenerate-container">
+                    <span className="collectionfreeformview-aiView-subtitle">Regenerate</span>
+                    <div className="collectionfreeformview-aiView-regenerate">
+                        <input
+                            className="collectionfreeformview-aiView-input"
+                            aria-label="Edit instructions input"
+                            type="text"
+                            value={this._regenInput}
+                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            placeholder="..under development.."
+                        />
+                        <Button
+                            text="Regenerate"
+                            type={Type.SEC}
+                            icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            // onClick={action(async () => {
+                            //     this._regenLoading = true;
+                            //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                            //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                            //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                            //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                            //     this._regenLoading = false;
+                            // })}
+                        />
+                    </div>
+                </div>
             </div>
         );
     };
@@ -2311,83 +2311,81 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     render() {
         TraceMobx();
         return (
-            <div>
-                <div
-                    className="collectionfreeformview-container"
-                    id={this._paintedId}
-                    ref={r => {
-                        this.createDashEventsTarget(r);
-                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                        this._oldWheel = r;
-                        // prevent wheel events from passivly propagating up through containers
-                        r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-                        r?.addEventListener('mouseleave', this.onMouseLeave);
-                        r?.addEventListener('mouseenter', this.onMouseEnter);
-                    }}
-                    onWheel={this.onPointerWheel}
-                    onClick={this.onClick}
-                    onPointerDown={this.onPointerDown}
-                    onPointerMove={this.onCursorMove}
-                    onDrop={this.onExternalDrop}
-                    onDragOver={e => e.preventDefault()}
-                    onContextMenu={this.onContextMenu}
-                    style={{
-                        pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(),
-                        textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
-                        transform: `scale(${this.nativeDimScaling})`,
-                        width: `${100 / this.nativeDimScaling}%`,
-                        height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
-                    }}>
-                    {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
-                        <div
-                            onPointerMove={this.onCursorMove}
-                            style={{
-                                position: 'fixed',
-                                left: this._eraserX,
-                                top: this._eraserY,
-                                width: (ActiveEraserWidth() + 5) * 2,
-                                height: (ActiveEraserWidth() + 5) * 2,
-                                borderRadius: '50%',
-                                border: '1px solid gray',
-                                transform: 'translate(-50%, -50%)',
-                            }}
+            <div
+                className="collectionfreeformview-container"
+                id={this._paintedId}
+                ref={r => {
+                    this.createDashEventsTarget(r);
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel = r;
+                    // prevent wheel events from passivly propagating up through containers
+                    r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                    r?.addEventListener('mouseleave', this.onMouseLeave);
+                    r?.addEventListener('mouseenter', this.onMouseEnter);
+                }}
+                onWheel={this.onPointerWheel}
+                onClick={this.onClick}
+                onPointerDown={this.onPointerDown}
+                onPointerMove={this.onCursorMove}
+                onDrop={this.onExternalDrop}
+                onDragOver={e => e.preventDefault()}
+                onContextMenu={this.onContextMenu}
+                style={{
+                    pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(),
+                    textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
+                    transform: `scale(${this.nativeDimScaling})`,
+                    width: `${100 / this.nativeDimScaling}%`,
+                    height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
+                }}>
+                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
+                    <div
+                        onPointerMove={this.onCursorMove}
+                        style={{
+                            position: 'fixed',
+                            left: this._eraserX,
+                            top: this._eraserY,
+                            width: (ActiveEraserWidth() + 5) * 2,
+                            height: (ActiveEraserWidth() + 5) * 2,
+                            borderRadius: '50%',
+                            border: '1px solid gray',
+                            transform: 'translate(-50%, -50%)',
+                        }}
+                    />
+                )}
+                {this.paintFunc ? (
+                    <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
+                ) : this._lightboxDoc ? (
+                    <div style={{ padding: 15, width: '100%', height: '100%' }}>
+                        <DocumentView
+                            {...this._props}
+                            Document={this._lightboxDoc}
+                            containerViewPath={this.DocumentView?.().docViewPath}
+                            TemplateDataDocument={undefined}
+                            PanelWidth={this.lightboxPanelWidth}
+                            PanelHeight={this.lightboxPanelHeight}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            onClickScript={this.onChildClickHandler}
+                            onKey={this.onKeyDown}
+                            onDoubleClickScript={this.onChildDoubleClickHandler}
+                            childFilters={this.childDocFilters}
+                            childFiltersByRanges={this.childDocRangeFilters}
+                            searchFilterDocs={this.searchFilterDocs}
+                            isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
+                            isContentActive={this._props.childContentsActive ?? emptyFunction}
+                            addDocTab={this.addDocTab}
+                            ScreenToLocalTransform={this.lightboxScreenToLocal}
+                            fitContentsToBox={undefined}
+                            focus={this.focus}
                         />
-                    )}
-                    {this.paintFunc ? (
-                        <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
-                    ) : this._lightboxDoc ? (
-                        <div style={{ padding: 15, width: '100%', height: '100%' }}>
-                            <DocumentView
-                                {...this._props}
-                                Document={this._lightboxDoc}
-                                containerViewPath={this.DocumentView?.().docViewPath}
-                                TemplateDataDocument={undefined}
-                                PanelWidth={this.lightboxPanelWidth}
-                                PanelHeight={this.lightboxPanelHeight}
-                                NativeWidth={returnZero}
-                                NativeHeight={returnZero}
-                                onClickScript={this.onChildClickHandler}
-                                onKey={this.onKeyDown}
-                                onDoubleClickScript={this.onChildDoubleClickHandler}
-                                childFilters={this.childDocFilters}
-                                childFiltersByRanges={this.childDocRangeFilters}
-                                searchFilterDocs={this.searchFilterDocs}
-                                isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
-                                isContentActive={this._props.childContentsActive ?? emptyFunction}
-                                addDocTab={this.addDocTab}
-                                ScreenToLocalTransform={this.lightboxScreenToLocal}
-                                fitContentsToBox={undefined}
-                                focus={this.focus}
-                            />
-                        </div>
-                    ) : (
-                        <>
-                            {this._firstRender ? this.placeholder : this.marqueeView}
-                            {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
-                            {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
-                        </>
-                    )}
-                </div>
+                    </div>
+                ) : (
+                    <>
+                        {this._firstRender ? this.placeholder : this.marqueeView}
+                        {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
+                        {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
+                    </>
+                )}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index a3d47290a..8e7307d23 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -280,7 +280,6 @@
 
 .documentView-editorView {
     width: 100%;
-    overflow-y: scroll;
     justify-items: center;
     background-color: rgb(223, 223, 223);
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 640d27aa1..e37658ca5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -85,7 +85,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView)
 }
 @observer
-export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
+export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() {
     // this makes mobx trace() statements more descriptive
     public get displayName()  { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
@@ -109,7 +109,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     private _mainCont = React.createRef<HTMLDivElement>();
     private _titleRef = React.createRef<EditableView>();
     private _dropDisposer?: DragManager.DragDropDisposer;
-    constructor(props: FieldViewProps & DocumentViewProps) {
+    constructor(props: FieldViewProps & DocumentViewProps & { showAIEditor: boolean }) {
         super(props);
         makeObservable(this);
     }
@@ -122,12 +122,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
-    @observable public _showAIEditor: boolean = false;
-
-    @action
-    showAIEditor() {
-        this._showAIEditor = !this._showAIEditor;
-    }
 
     get _contentDiv()  { return this._mainCont.current; } // prettier-ignore
     get _docView()     { return this._props.DocumentView?.(); } // prettier-ignore
@@ -689,7 +683,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     rootSelected = () => this._rootSelected;
     panelHeight = () => this._props.PanelHeight() - this.headerMargin;
-    screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
+    screenToLocalContent = () =>
+        this._props
+            .ScreenToLocalTransform()
+            .translate(0, -this.headerMargin)
+            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.rph() : 1);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
     setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
     setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
@@ -715,9 +713,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    @observable _aiWinHeight = 100;
-    rpw = () => this._props.PanelWidth() - this._aiWinHeight;
-    rph = () => this.panelHeight() - this._aiWinHeight;
+    @observable _aiWinHeight = 95;
+    rpw = () => (this.rph() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
+    rph = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight);
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -728,8 +726,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                        width: this._showAIEditor ? this.rpw() : undefined,
-                        height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        width: this._props.showAIEditor ? this.rpw() : undefined,
+                        height: this._props.showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -737,8 +735,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
                         childFilters={this.childFilters}
-                        PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
-                        PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
+                        PanelWidth={this._props.showAIEditor ? this.rpw : this._props.PanelWidth}
+                        PanelHeight={this._props.showAIEditor ? this.rph : this.panelHeight}
                         setHeight={this.setHeight}
                         isContentActive={this.isContentActive}
                         ScreenToLocalTransform={this.screenToLocalContent}
@@ -748,7 +746,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {!this._showAIEditor ? null : (
+                {!this._props.showAIEditor ? null : (
                     <>
                         <div
                             className="documentView-editorView-history"
@@ -1168,10 +1166,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     @computed private get nativeScaling() {
         if (this.shouldNotScale) return 1;
         const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
-        if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
-            return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth
+        const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0;
+        const effNW = Math.max(this.effectiveNativeWidth - ai, 1);
+        const effNH = Math.max(this.effectiveNativeHeight - ai, 1);
+        if (this.layout_fitWidth || (this._props.PanelHeight() - ai) / effNH > (this._props.PanelWidth() - ai) / effNW) {
+            return Math.max(minTextScale, (this._props.PanelWidth() - ai) / effNW); // width-limited or layout_fitWidth
         }
-        return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
+        return Math.max(minTextScale, (this._props.PanelHeight() - ai) / effNH); // height-limited or unscaled
     }
     @computed private get panelWidth() {
         return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth();
@@ -1231,7 +1232,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 
     @computed get layout_fitWidth() {
-        return this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
+        return this._showAIEditor ? false : (this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth);
     }
     @computed get anchorViewDoc() {
         return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined;
@@ -1327,9 +1328,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
     };
 
+    @observable public _showAIEditor: boolean = false;
+
     @action
     public toggleAIEditor = () => {
-        this._docViewInternal && this._docViewInternal.showAIEditor();
+        this._showAIEditor = !this._showAIEditor;
     };
 
     public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
@@ -1516,6 +1519,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         }}>
                         <DocumentViewInternal
                             {...this._props}
+                            showAIEditor={this._showAIEditor}
                             reactParent={undefined}
                             isHovering={this.isHovering}
                             fieldKey={this.LayoutFieldKey}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 1b6b3c85a..a50320f5e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,6 +40,7 @@
     max-height: 100%;
     pointer-events: inherit;
     background: transparent;
+    z-index: -10000;
 
     img {
         height: auto;
@@ -159,6 +160,7 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
@@ -168,20 +170,29 @@
     .imageBox-aiView-regenerate-container,
     .imageBox-aiView-options-container {
         font-weight: normal;
-        margin: 5px;
         display: flex;
     }
 
     .imageBox-aiView-regenerate,
     .imageBox-aiView-options {
         display: flex;
-        flex-direction: row;
-        justify-content: center;
+        align-items: center;
         flex-direction: row;
         gap: 5px;
+        width: 100%;
     }
 
+    .imageBox-aiView-strength {
+        text-align: center;
+        align-items: center;
+        display: flex;
+        width: 125px;
+    }
+    .imageBox-aiView-slider {
+        width: 50px;
+        margin-left: 5px;
+    }
     .imageBox-aiView-input {
-        width: 50%;
+        width: 100%;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9838af4d0..9223fc180 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
+import { Slider, Tooltip } from '@mui/material';
 import axios from 'axios';
 import { Colors, Button, Type, Size } from '@dash/components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
@@ -44,6 +44,7 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { SettingsManager } from '../../util/SettingsManager';
 import { AiOutlineSend } from 'react-icons/ai';
 import { FireflyImageData } from '../smartdraw/FireflyConstants';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -98,7 +99,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = [];
+    @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -386,8 +387,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             : UpdateIcon(
                   this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
                   contentDiv,
-                  usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                  usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+                  usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+                  usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
                   this._props.PanelWidth(),
                   this._props.PanelHeight(),
                   0,
@@ -543,65 +544,91 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <div key={img.pathname}>
                         <img
                             className="imageBox-aiView-img"
-                            src={img.href}
+                            src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
                             onClick={() => {
                                 this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
                                 this.dataDoc.ai_firefly_prompt = img.prompt;
                                 this.dataDoc.ai_firefly_seed = img.seed;
                             }}
                         />
-                        <text>{img.prompt}</text>
+                        <span>{img.prompt}</span>
                     </div>
                 ))}
             </div>
         );
     };
+    @observable private _fireflyRefStrength = 0;
 
     componentAIView = () => {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
-        const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
-                {showRegenerate && (
-                    <div className="imageBox-aiView-regenerate-container">
-                        <div className="imageBox-aiView-regenerate">
-                            <input
-                                className="imageBox-aiView-input"
-                                aria-label="Edit instructions input"
-                                type="text"
-                                value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                placeholder="Prompt (Optional)"
-                            />
-                            <Button
-                                text="Regenerate Image"
-                                type={Type.SEC}
-                                // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => {
-                                        if (newImgs[0]) {
-                                            const url = newImgs[0].pathname;
-                                            const imgField = new ImageField(url);
-                                            this._prevImgs.length === 0 &&
-                                                this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
-                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
-                                            this.dataDoc[this.fieldKey] = imgField;
-                                            this._regenerateLoading = false;
-                                            this._regenInput = '';
-                                        }
-                                    });
-                                })}
+                <div className="imageBox-aiView-regenerate-container">
+                    <div className="imageBox-aiView-regenerate">
+                        Firefly:
+                        <input
+                            className="imageBox-aiView-input"
+                            aria-label="Edit instructions input"
+                            type="text"
+                            value={this._regenInput}
+                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            placeholder={this._regenInput || StrCast(this.Document.title)}
+                        />
+                        <div className="imageBox-aiView-strength">
+                            <span style={{ width: 60 }}>Similarity</span>
+                            <Slider
+                                className="imageBox-aiView-slider"
+                                sx={{
+                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                }}
+                                min={0}
+                                max={100}
+                                step={1}
+                                size="small"
+                                value={this._fireflyRefStrength}
+                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                valueLabelDisplay="auto"
                             />
-                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
+                        <Button
+                            text="Create"
+                            type={Type.SEC}
+                            // style={{ alignSelf: 'flex-end' }}
+                            icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={action(async () => {
+                                this._regenerateLoading = true;
+                                if (this._fireflyRefStrength) {
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(
+                                        action(() => {
+                                            this._regenerateLoading = false;
+                                        })
+                                    );
+                                } else
+                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
+                                        action(newImgs => {
+                                            if (newImgs[0]) {
+                                                const url = newImgs[0].pathname;
+                                                const imgField = new ImageField(url);
+                                                this._prevImgs.length === 0 &&
+                                                    this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
+                                                this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url });
+                                                this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
+                                                this.dataDoc.ai_firefly_prompt = newImgs[0].prompt;
+                                                this.dataDoc[this.fieldKey] = imgField;
+                                                this._regenerateLoading = false;
+                                                this._regenInput = '';
+                                            }
+                                        })
+                                    );
+                            })}
+                        />
                     </div>
-                )}
+                </div>
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
+                    <span className="imageBox-aiView-subtitle"> More Options: </span>
                     <div className="imageBox-aiView-options">
                         <Button
                             type={Type.TERT}
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 423e3b2ac..d9a01c436 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -15,7 +15,7 @@ export class DrawingFillHandler {
         const docData = drawing[DocData];
         const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
-        DocumentView.GetDocImage(drawing)?.then(imageField => {
+        return DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
                 let dims: { width: number; height: number };
@@ -31,18 +31,17 @@ export class DrawingFillHandler {
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
-                imageUrlToBase64(structureUrl)
-                    .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
-                    .then(prompt => {
+                return imageUrlToBase64(structureUrl)
+                    .then(gptDescribeImage)
+                    .then(prompt => 
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
-                                    { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
-                        ).catch(e => alert("create image failed: " + e.toString())); // prettier-ignore
-                    });
+                                    { ai: 'firefly', title: user_prompt || prompt, ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
+                        ).catch(e => alert("create image failed: " + e.toString())) 
+                    ); // prettier-ignore
             }
-            return false;
         });
     };
 }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index f635b5642..7db9ef133 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -271,16 +271,16 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
-        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed })
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed })
             .then(img => {
-                const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+                const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
                 if (!changeInPlace) {
                     const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                         title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
                         nativeWidth: dims.width,
                         nativeHeight: dims.height,
                         ai: 'firefly',
-                        ai_firefly_seed: seed,
+                        ai_firefly_seed: newseed,
                         ai_firefly_prompt: input,
                     });
                     DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
@@ -306,7 +306,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     case DocumentType.IMG:
                         if (this._regenInput) {
                             // if (this._selectedDoc) {
-                            const newPrompt = `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}`;
+                            const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
                             return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace);
                             // }
                         }
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 5a880901b..c9d5df547 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -274,14 +274,20 @@ export default class UploadManager extends ApiManager {
                         .filter(f => regex.test(f))
                         .map(f => fs.unlinkSync(serverPath + f));
                 }
-                imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
-                    const ext = path.extname(savedName).toLowerCase();
-                    const outputPath = serverPathToFile(Directory.images, filename + ext);
-                    if (AcceptableMedia.imageFormats.includes(ext)) {
-                        workerResample(savedName, outputPath, origSuffix, false);
-                    }
-                    res.send(clientPathToFile(Directory.images, filename + ext));
-                });
+                imageDataUri
+                    .outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix)))
+                    .then((savedName: string) => {
+                        const ext = path.extname(savedName).toLowerCase();
+                        const outputPath = serverPathToFile(Directory.images, filename + ext);
+                        if (AcceptableMedia.imageFormats.includes(ext)) {
+                            workerResample(savedName, outputPath, origSuffix, false);
+                        }
+                        res.send(clientPathToFile(Directory.images, filename + ext));
+                    })
+                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+                    .catch((e: any) => {
+                        res.status(404).json({ error: e.toString() });
+                    });
             },
         });
     }
-- 
cgit v1.2.3-70-g09d2


From 156a2040cc51397c3656615f9133279dac900c82 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 17 Jan 2025 11:17:15 -0500
Subject: changed to generating 4 images for each firefly request.

---
 src/client/views/nodes/DocumentView.tsx           |  4 ++--
 src/client/views/smartdraw/DrawingFillHandler.tsx | 28 ++++++++++++-----------
 src/server/ApiManagers/FireflyManager.ts          | 22 ++++++++++--------
 3 files changed, 29 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e37658ca5..bc3f27124 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1110,10 +1110,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      * @param doc Doc to snapshot
      * @returns promise of icon ImageField
      */
-    public static GetDocImage(doc: Doc) {
+    public static GetDocImage(doc?: Doc) {
         return DocumentView.getDocumentView(doc)
             ?.ComponentView?.updateIcon?.()
-            .then(() => ImageCast(doc?.icon, ImageCast(doc[Doc.LayoutFieldKey(doc)])));
+            .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)])));
     }
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 16ac2b79e..d0a840883 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,7 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { DocCast, ImageCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
@@ -48,20 +48,22 @@ export class DrawingFillHandler {
                     .then(gptDescribeImage)
                     .then((prompt, newPrompt = user_prompt || prompt) =>
                         Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
-                            .then((info: Upload.ImageInformation) => {
+                            .then((infos: Upload.ImageInformation[]) => {
                                 const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 });
                                 drawing[DocData].ai_firefly_generatedDocs = genratedDocs;
-                                Doc.AddDocToList(
-                                    genratedDocs,
-                                    undefined,
-                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
-                                        ai: 'firefly',
-                                        title: newPrompt,
-                                        ai_firefly_prompt: newPrompt,
-                                        _width: 500,
-                                        data_nativeWidth: info.nativeWidth,
-                                        data_nativeHeight: info.nativeHeight,
-                                    })
+                                infos.map(info =>
+                                    Doc.AddDocToList(
+                                        genratedDocs,
+                                        undefined,
+                                        Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                            ai: 'firefly',
+                                            title: newPrompt,
+                                            ai_firefly_prompt: newPrompt,
+                                            _width: 500,
+                                            data_nativeWidth: info.nativeWidth,
+                                            data_nativeHeight: info.nativeHeight,
+                                        })
+                                    )
                                 );
                                 if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
                             })
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index f49cf4132..160a94d40 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -36,8 +36,9 @@ export default class FireflyManager extends ApiManager {
                     ],
                     body: JSON.stringify({
                         prompt,
-                        // detailLevel: 'preview', 
-                        // modelVersion: 'image3_fast',
+                        numVariations: 4,
+                        detailLevel: 'preview', 
+                        modelVersion: 'image3_fast',
                         size: { width, height },
                         structure: !structureUrl
                             ? undefined
@@ -61,7 +62,7 @@ export default class FireflyManager extends ApiManager {
                 .then(response2 => response2.json().then(json => 
                 {
                     if (json.outputs?.length)
-                        return JSON.stringify((json.outputs?.[0] as { image: { url: string } } ?? {})?.image);
+                        return (json.outputs as {image: {url:string }}[]).map(output => output.image);
                     throw new Error(JSON.stringify(json));
                 })
             )
@@ -306,13 +307,14 @@ export default class FireflyManager extends ApiManager {
                                 })
                                 .then(uploads =>
                                     this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl)
-                                        .then(fire =>
-                                            DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                                                if (info instanceof Error) _invalid(res, info.message);
-                                                else _success(res, info);
-                                                resolver();
-                                            })
-                                        )
+                                        .then(images => {
+                                            Promise.all((images ?? [new Error('no images were generated')]).map(fire => (fire instanceof Error ? fire : DashUploadUtils.UploadImage(fire.url))))
+                                                .then(dashImages => {
+                                                    if (dashImages.every(img => img instanceof Error)) _invalid(res, dashImages[0]!.message);
+                                                    else _success(res, JSON.stringify(dashImages.filter(img => !(img instanceof Error))));
+                                                })
+                                                .then(resolver);
+                                        })
                                         .catch(e => {
                                             _invalid(res, e.message);
                                             resolver();
-- 
cgit v1.2.3-70-g09d2


From e4fca20fd4f7d7401fb62d5f700a15c604070484 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 19 Jan 2025 14:35:23 -0500
Subject: css fixes for ai editor views.

---
 src/client/views/TagsView.scss                     |  20 ++-
 src/client/views/TagsView.tsx                      |  18 +--
 .../collectionFreeForm/CollectionFreeFormView.scss |  27 +++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  78 +++++----
 src/client/views/nodes/DocumentView.scss           |   7 +
 src/client/views/nodes/DocumentView.tsx            |  76 ++++++---
 src/client/views/nodes/ImageBox.scss               |  42 ++++-
 src/client/views/nodes/ImageBox.tsx                | 180 ++++++++++-----------
 8 files changed, 264 insertions(+), 184 deletions(-)

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

diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss
index b741e7bfd..303a08e1e 100644
--- a/src/client/views/TagsView.scss
+++ b/src/client/views/TagsView.scss
@@ -4,13 +4,19 @@
     flex-direction: column;
     border: 1px solid;
     border-radius: 4px;
-}
-
-.tagsView-list {
-    display: flex;
-    flex-wrap: wrap;
-    .iconButton-container {
-        min-height: unset !important;
+    width: 100%;
+    position: relative;
+    .tagsView-content {
+        width: 100%;
+        height: inherit;
+        .tagsView-list {
+            display: flex;
+            flex-wrap: wrap;
+            height: inherit;
+            .iconButton-container {
+                min-height: unset !important;
+            }
+        }
     }
 }
 
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 0edd89204..73f404596 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -295,15 +295,6 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
         return this._props.Views.lastElement();
     }
 
-    // x: 1 => 1/vs   0 => 1      1/(vs - (1-x)*(vs-1))
-    @computed get currentScale() {
-        if (this._props.Views.length > 1) return 1;
-        const x = NumCast(this.View.Document.height) / this.View.screenToContentsTransform().Scale / 80;
-        const xscale = x >= 1 ? 0 : 1 / (1 + x * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35  /  this.View.screenToLocalScale() - ;
-        const y = NumCast(this.View.Document.width) / this.View.screenToContentsTransform().Scale / 200;
-        const yscale = y >= 1 ? 0 : 1 / (1 + y * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35  /  this.View.screenToLocalScale() - ;
-        return Math.max(xscale, yscale, 1 / this.View.screenToLocalScale());
-    }
     @computed get isEditing() {
         return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View)));
     }
@@ -359,16 +350,11 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                 ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
                 style={{
                     display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
-                    transformOrigin: 'top left',
-                    maxWidth: `${100 * this.currentScale}%`,
-                    width: `${100 * this.currentScale}%`,
-                    transform: `scale(${1 / this.currentScale})`,
                     backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
                     borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
-                    position: 'relative',
-                    top: this._props.Views.lastElement()._showAIEditor ? undefined : this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+                    height: !this._props.Views.lastElement()?.isSelected() ? 0 : undefined,
                 }}>
-                <div className="tagsView-content" style={{ width: '100%' }}>
+                <div className="tagsView-content">
                     <div className="tagsView-list">
                         {this._props.Views.length === 1 && !this.View.showTags ? null : ( //
                             <IconButton
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 6dabcb8b2..46bd37f6d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -312,14 +312,28 @@
 
     .collectionfreeformview-aiView-prompt {
         height: 25px;
-        width: 100%;
+        width: 65%;
     }
 
     .collectionFreeFormView-aiView-strength {
         text-align: center;
         align-items: center;
         display: flex;
-        width: 150px;
+        width: 25%;
+        .collectionFreeFormView-aiView-similarity {
+            max-width: 65px;
+            width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+
+    .collectionFreeForView-aiView-send {
+        width: 10%;
+        .button-container {
+            width: 100% !important;
+            justify-content: left !important;
+        }
     }
 
     .collectionFreeformView-aiView-options-container,
@@ -341,5 +355,14 @@
         align-items: center;
         width: 100%;
         gap: 10px;
+        .collectionFreeformView-aiView-input {
+            width: 100%;
+        }
+        .collectionFreeFormView-aiView-regenBtn {
+            width: 10%;
+            .button-container {
+                width: 100% !important;
+            }
+        }
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eeb97bf15..796949378 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2209,10 +2209,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
                             type="text"
                             value={this._drawingFillInput}
-                            onChange={action(e => {this._drawingFillInput = e.target.value})}
+                            onChange={action(e => {
+                                this._drawingFillInput = e.target.value;
+                            })}
                         />
-                        <div className="collectionfreeformview-aiView-strength">
-                            Similarity
+                        <div className="collectionFreeFormView-aiView-strength">
+                            <span className="collectionFreeFormView-aiView-similarity">Similarity</span>
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
                                 sx={{
@@ -2229,23 +2231,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 valueLabelDisplay="auto"
                             />
                         </div>
-                        <Button
-                            text="Send"
-                            type={Type.SEC}
-                            icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                            iconPlacement="right"
-                            onClick={undoable(
-                                action(() => {
-                                    this._drawingFillLoading = true;
-                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
-                                        action(() => {
-                                            this._drawingFillLoading = false;
-                                        })
-                                    );
-                                }),
-                                'create image'
-                            )}
-                        />
+                        <div className="collectionFreeFormView-aiView-send">
+                            <Button
+                                text="Send"
+                                type={Type.SEC}
+                                icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                onClick={undoable(
+                                    action(() => {
+                                        this._drawingFillLoading = true;
+                                        DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
+                                            action(() => {
+                                                this._drawingFillLoading = false;
+                                            })
+                                        );
+                                    }),
+                                    'create image'
+                                )}
+                            />
+                        </div>
                     </div>
                 </div>
                 <div className="collectionfreeformview-aiView-regenerate-container">
@@ -2256,23 +2260,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             aria-label="Edit instructions input"
                             type="text"
                             value={this._regenInput}
-                            onChange={action(e => {this._regenInput = e.target.value})}
+                            onChange={action(e => {
+                                this._regenInput = e.target.value;
+                            })}
                             placeholder="..under development.."
                         />
-                        <Button
-                            text="Regenerate"
-                            type={Type.SEC}
-                            icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                            iconPlacement="right"
-                            // onClick={action(async () => {
-                            //     this._regenLoading = true;
-                            //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
-                            //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                            //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                            //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
-                            //     this._regenLoading = false;
-                            // })}
-                        />
+                        <div className="collectionFreeFormView-aiView-regenBtn">
+                            <Button
+                                text="Regenerate"
+                                type={Type.SEC}
+                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                // onClick={action(async () => {
+                                //     this._regenLoading = true;
+                                //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                                //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                                //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                                //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                                //     this._regenLoading = false;
+                                // })}
+                            />
+                        </div>
                     </div>
                 </div>
             </div>
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index cd813cb67..7e5507586 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -271,8 +271,14 @@
     }
 }
 
+.documentView-noAIWidgets {
+    transform-origin: top left;
+    position: relative;
+}
+
 .documentView-editorView-history {
     position: absolute;
+    transform-origin: top right;
     right: 0;
     top: 0;
     overflow-y: scroll;
@@ -284,6 +290,7 @@
     scrollbar-width: thin;
     justify-items: center;
     background-color: rgb(223, 223, 223);
+    transform-origin: top left;
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bc3f27124..83c948f0b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -551,6 +551,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
+            appearanceItems.push({ description: 'AI view', event: () => this._docView?.toggleAIEditor(), icon: 'map-pin' });
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -687,7 +688,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         this._props
             .ScreenToLocalTransform()
             .translate(0, -this.headerMargin)
-            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.rph() : 1);
+            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
     setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
     setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
@@ -713,9 +714,32 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    @observable _aiWinHeight = 95;
-    rpw = () => (this.rph() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
-    rph = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight);
+    @observable _aiWinHeight = 88;
+
+    private _sideBtnWidth = 22;
+    @computed get currentScale() {
+        const viewXfScale = this._props.DocumentView!().screenToLocalScale();
+        const x = NumCast(this.Document.height) / viewXfScale / 80;
+        const xscale = x >= 1 ? 0 : 1 / (1 + x * (viewXfScale - 1));
+        const y = NumCast(this.Document.width) / viewXfScale / 200;
+        const yscale = y >= 1 ? 0 : 1 / (1 + y * viewXfScale - 1);
+        return Math.max(xscale, yscale, 1 / viewXfScale);
+    }
+    /**
+     * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get viewScaling() { return 1 / this.currentScale; } // prettier-ignore
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaling() {  return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
+
+    aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
+    aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling);
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -726,8 +750,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                        width: this._props.showAIEditor ? this.rpw() : undefined,
-                        height: this._props.showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        width: this._props.showAIEditor ? this.aiContentsWidth() : undefined,
+                        height: this._props.showAIEditor ? this.aiContentsHeight() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -735,8 +759,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
                         childFilters={this.childFilters}
-                        PanelWidth={this._props.showAIEditor ? this.rpw : this._props.PanelWidth}
-                        PanelHeight={this._props.showAIEditor ? this.rph : this.panelHeight}
+                        PanelWidth={this._props.showAIEditor ? this.aiContentsWidth : this._props.PanelWidth}
+                        PanelHeight={this._props.showAIEditor ? this.aiContentsHeight : this.panelHeight}
                         setHeight={this.setHeight}
                         isContentActive={this.isContentActive}
                         ScreenToLocalTransform={this.screenToLocalContent}
@@ -746,23 +770,38 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {!this._props.showAIEditor ? null : (
+                {!this._props.showAIEditor ? (
+                    <div
+                        className="documentView-noAiWidgets"
+                        style={{
+                            width: `${100 / this.uiBtnScaling}%`, //
+                            transform: `scale(${this.uiBtnScaling})`,
+                            bottom: this.maxWidgetSize,
+                        }}>
+                        {this.widgetDecorations ?? null}
+                    </div>
+                ) : (
                     <>
                         <div
                             className="documentView-editorView-history"
-                            ref={r => {
-                                this.historyRef(this._oldAiWheel, (this._oldAiWheel = r));
-                            }}
-                            style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) * 0.95 }}>
+                            ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))}
+                            style={{
+                                transform: `scale(${this.uiBtnScaling})`,
+                                height: this.aiContentsHeight() / this.uiBtnScaling,
+                                width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling,
+                            }}>
                             {this._componentView?.componentAIViewHistory?.() ?? null}
                         </div>
                         <div
                             className="documentView-editorView"
-                            ref={r => {
-                                this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r));
-                            }}>
+                            style={{
+                                width: `${100 / this.uiBtnScaling}%`, //
+                                transform: `scale(${this.uiBtnScaling})`,
+                            }}
+                            ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
                             <div className="documentView-editorView-resizer" />
                             {this._componentView?.componentAIView?.() ?? null}
+                            {this.widgetDecorations ?? null}
                         </div>
                     </>
                 )}
@@ -934,7 +973,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                               {this.captionView}
                           </div>
                       )}
-                      {this.widgetDecorations ?? null}
                   </div>
               ));
     };
@@ -1232,7 +1270,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 
     @computed get layout_fitWidth() {
-        return this._showAIEditor ? false : (this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth);
+        return this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
     }
     @computed get anchorViewDoc() {
         return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined;
@@ -1430,7 +1468,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]);
 
     layout_fitWidthFunc = (/* doc: Doc */) => BoolCast(this.layout_fitWidth);
-    screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
+    screenToLocalScale = () => this.screenToViewTransform().Scale;
     isSelected = () => this.IsSelected;
     select = (extendSelection: boolean, focusSelection?: boolean) => {
         if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection);
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 29b24b774..fe4f0b1a2 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -164,16 +164,15 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    transform-origin: top left;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
         align-content: center;
-    }
-
-    .imageBox-aiView-regenerate-container,
-    .imageBox-aiView-options-container {
-        font-weight: normal;
-        display: flex;
+        max-width: 10%;
+        overflow: hidden;
+        text-overflow: ellipsis;
     }
 
     .imageBox-aiView-regenerate,
@@ -183,19 +182,46 @@
         flex-direction: row;
         gap: 5px;
         width: 100%;
+        .imageBox-aiView-regenerate-createBtn {
+            max-width: 10%;
+            .button-container {
+                width: 100% !important;
+                justify-content: left !important;
+            }
+        }
+    }
+
+    .imageBox-aiView-firefly {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 10%;
+        width: 100%;
+    }
+    .imageBox-aiView-regenerate-send {
+        max-width: 10%;
     }
 
     .imageBox-aiView-strength {
         text-align: center;
         align-items: center;
         display: flex;
-        width: 125px;
+        max-width: 25%;
+        width: 100%;
+        .imageBox-aiView-similarity {
+            max-width: 65;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            width: 100%;
+        }
     }
     .imageBox-aiView-slider {
-        width: 50px;
+        width: 90%;
         margin-left: 5px;
     }
     .imageBox-aiView-input {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 65%;
         width: 100%;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 82d8ea5f1..caefbf542 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -541,10 +541,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     protected _sideBtnMaxPanelPct = 0.12;
     @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
     @observable private _fireflyRefStrength = 0;
-    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
-    @computed get maxWidgetSize() { return Math.min(this._btnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width) ? 1 : this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
-    @computed get uiBtnScaling() { return this.maxWidgetSize / this._btnWidth; } // prettier-ignore
-    @computed get uiInputScaling() { return this.maxWidgetSize / this._inputWidth; } // prettier-ignore
 
     componentAIViewHistory = () => (
         <div className="imageBox-aiView-history">
@@ -570,35 +566,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         return (
             <div className="imageBox-aiView">
-                <div className="imageBox-aiView-regenerate-container">
-                    <div className="imageBox-aiView-regenerate">
-                        Firefly:
-                        <input
-                            className="imageBox-aiView-input"
-                            aria-label="Edit instructions input"
-                            type="text"
-                            value={this._regenInput}
-                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                            placeholder={this._regenInput || StrCast(this.Document.title)}
+                <div className="imageBox-aiView-regenerate">
+                    <span className="imageBox-aiView-firefly">Firefly:</span>
+                    <input
+                        className="imageBox-aiView-input"
+                        aria-label="Edit instructions input"
+                        type="text"
+                        value={this._regenInput}
+                        onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                        placeholder={this._regenInput || StrCast(this.Document.title)}
+                    />
+                    <div className="imageBox-aiView-strength">
+                        <span className="imageBox-aiView-similarity">Similarity</span>
+                        <Slider
+                            className="imageBox-aiView-slider"
+                            sx={{
+                                '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                            }}
+                            min={0}
+                            max={100}
+                            step={1}
+                            size="small"
+                            value={this._fireflyRefStrength}
+                            onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                            valueLabelDisplay="auto"
                         />
-                        <div className="imageBox-aiView-strength">
-                            <span style={{ width: 60 }}>Similarity</span>
-                            <Slider
-                                className="imageBox-aiView-slider"
-                                sx={{
-                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
-                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
-                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
-                                }}
-                                min={0}
-                                max={100}
-                                step={1}
-                                size="small"
-                                value={this._fireflyRefStrength}
-                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
-                                valueLabelDisplay="auto"
-                            />
-                        </div>
+                    </div>
+                    <div className="imageBox-aiView-regenerate-createBtn">
                         <Button
                             text="Create"
                             type={Type.SEC}
@@ -634,71 +630,61 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         />
                     </div>
                 </div>
-                <div className="imageBox-aiView-options-container">
-                    <span className="imageBox-aiView-subtitle"> More Options: </span>
-                    <div className="imageBox-aiView-options">
-                        {SmartDrawHandler.Instance.ShowRegenerate && (
-                            <text className="imageBox-aiView-subtitle" style={{ transform: `scale(${this.uiBtnScaling})` }}>
-                                More Options:
-                            </text>
-                        )}
-                        <Button
-                            style={{ transform: `scale(${this.uiBtnScaling})` }}
-                            type={Type.TERT}
-                            text="Get Text"
-                            icon={<FontAwesomeIcon icon="font" />}
-                            color={SettingsManager.userBackgroundColor}
-                            iconPlacement="right"
-                            onClick={() => {
-                                Networking.PostToServer('/queryFireflyImageText', {
-                                    file: (file => {
-                                        const ext = extname(file);
-                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
-                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
-                                }).then(text => alert(text));
-                            }}
-                        />
-                        <Button
-                            style={{ transform: `scale(${this.uiBtnScaling})` }}
-                            type={Type.TERT}
-                            text="Generative Fill"
-                            icon={<FontAwesomeIcon icon="fill" />}
-                            color={SettingsManager.userBackgroundColor}
-                            iconPlacement="right"
-                            onClick={action(() => {
-                                ImageEditorData.Open = true;
-                                ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
-                                ImageEditorData.AddDoc = this._props.addDocument;
-                                ImageEditorData.RootDoc = this.Document;
-                            })}
-                        />
-                        <Button
-                            style={{ transform: `scale(${this.uiBtnScaling})` }}
-                            type={Type.TERT}
-                            text="Expand"
-                            icon={<FontAwesomeIcon icon="expand" />}
-                            color={SettingsManager.userBackgroundColor}
-                            iconPlacement="right"
-                            onClick={() => {
-                                Networking.PostToServer('/expandImage', {
-                                    prompt: 'sunny skies',
-                                    file: (file => {
-                                        const ext = extname(file);
-                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
-                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
-                                }).then((info: Upload.ImageInformation) => {
-                                    const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
-                                    DocUtils.assignImageInfo(info, img);
-                                    const genratedDocs = this.Document.generatedDocs
-                                        ? DocCast(this.Document.generatedDocs)
-                                        : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) });
-                                    Doc.AddDocToList(genratedDocs, undefined, img);
-                                    this.Document[DocData].generatedDocs = genratedDocs;
-                                    if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight);
-                                });
-                            }}
-                        />
-                    </div>
+                <div className="imageBox-aiView-options">
+                    <span className="imageBox-aiView-subtitle"> More: </span>
+                    <Button
+                        type={Type.TERT}
+                        text="Get Text"
+                        icon={<FontAwesomeIcon icon="font" />}
+                        color={SettingsManager.userBackgroundColor}
+                        iconPlacement="right"
+                        onClick={() => {
+                            Networking.PostToServer('/queryFireflyImageText', {
+                                file: (file => {
+                                    const ext = extname(file);
+                                    return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                            }).then(text => alert(text));
+                        }}
+                    />
+                    <Button
+                        type={Type.TERT}
+                        text="Generative Fill"
+                        icon={<FontAwesomeIcon icon="fill" />}
+                        color={SettingsManager.userBackgroundColor}
+                        iconPlacement="right"
+                        onClick={action(() => {
+                            ImageEditorData.Open = true;
+                            ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
+                            ImageEditorData.AddDoc = this._props.addDocument;
+                            ImageEditorData.RootDoc = this.Document;
+                        })}
+                    />
+                    <Button
+                        type={Type.TERT}
+                        text="Expand"
+                        icon={<FontAwesomeIcon icon="expand" />}
+                        color={SettingsManager.userBackgroundColor}
+                        iconPlacement="right"
+                        onClick={() => {
+                            Networking.PostToServer('/expandImage', {
+                                prompt: 'sunny skies',
+                                file: (file => {
+                                    const ext = extname(file);
+                                    return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                            }).then((info: Upload.ImageInformation) => {
+                                const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+                                DocUtils.assignImageInfo(info, img);
+                                const genratedDocs = this.Document.generatedDocs
+                                    ? DocCast(this.Document.generatedDocs)
+                                    : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) });
+                                Doc.AddDocToList(genratedDocs, undefined, img);
+                                this.Document[DocData].generatedDocs = genratedDocs;
+                                if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight);
+                            });
+                        }}
+                    />
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From 000773de9807f3932d9f0a223779f38767a982fc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 19 Jan 2025 14:36:03 -0500
Subject: from last

---
 src/client/views/nodes/DocumentView.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 83c948f0b..f1b48be4a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -716,7 +716,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     @observable _aiWinHeight = 88;
 
-    private _sideBtnWidth = 22;
+    private _tagsBtnHeight = 22;
     @computed get currentScale() {
         const viewXfScale = this._props.DocumentView!().screenToLocalScale();
         const x = NumCast(this.Document.height) / viewXfScale / 80;
@@ -732,11 +732,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     /**
      * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
      */
-    @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
+    @computed get maxWidgetSize() { return Math.min(this._tagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
     /**
      * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
      */
-    @computed get uiBtnScaling() {  return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
+    @computed get uiBtnScaling() {  return Math.max(this.maxWidgetSize / this._tagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
 
     aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
     aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling);
-- 
cgit v1.2.3-70-g09d2


From 406706ab42bc1fdac81b3bffdcadfcfe05cfc8a0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 27 Jan 2025 12:06:50 -0500
Subject: fixed placement of widget decorations

---
 src/client/views/StyleProvider.tsx      | 9 +++------
 src/client/views/nodes/DocumentView.tsx | 6 ++++--
 2 files changed, 7 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index e825a27d3..bebc9a341 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,7 +1,7 @@
+import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components';
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components';
 import { action, untracked } from 'mobx';
 import { extname } from 'path';
 import * as React from 'react';
@@ -10,6 +10,7 @@ import { FaFilter } from 'react-icons/fa';
 import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
+import { InkInkTool } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
 import { AudioAnnoState } from '../../server/SharedMediaTypes';
@@ -21,11 +22,9 @@ import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
 import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
-import { styleProviderQuiz } from './StyleProviderQuiz';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
-import { TagsView } from './TagsView';
-import { InkInkTool } from '../../fields/InkField';
+import { styleProviderQuiz } from './StyleProviderQuiz';
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -394,7 +393,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     </Tooltip>
                 );
             };
-            const tags = () => docView?.() ? <TagsView Views={[docView?.()]}/> :  null;
         
             return (
                 <>
@@ -402,7 +400,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     {lock()}
                     {filter()}
                     {audio()}
-                    {tags()}
                 </>
             );
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f1b48be4a..5fd9223e3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
+import { TagsView } from '../TagsView';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -778,7 +779,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                             transform: `scale(${this.uiBtnScaling})`,
                             bottom: this.maxWidgetSize,
                         }}>
-                        {this.widgetDecorations ?? null}
+                        {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
                     </div>
                 ) : (
                     <>
@@ -801,10 +802,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                             ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
                             <div className="documentView-editorView-resizer" />
                             {this._componentView?.componentAIView?.() ?? null}
-                            {this.widgetDecorations ?? null}
+                            {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
                         </div>
                     </>
                 )}
+                {this.widgetDecorations ?? null}
             </>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From f66eaad00dbfe27e8dbe28019d1148a70f6ba0d9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 3 Feb 2025 12:44:38 -0500
Subject: fixed defaults for roll animations effect.  fixed hideBefore pres
 effect to not show Doc before effect starts

---
 src/client/util/DocumentManager.ts         | 29 +++++++++-----------
 src/client/views/nodes/DocumentView.tsx    | 12 ++++-----
 src/client/views/nodes/FocusViewOptions.ts | 11 ++++++++
 src/client/views/nodes/trails/PresBox.tsx  | 43 ++++++++++++++----------------
 4 files changed, 50 insertions(+), 45 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 286112bf9..acb35f7eb 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -8,7 +8,7 @@ import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { CollectionViewType } from '../documents/DocumentTypes';
 import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
-import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
+import { FocusEffectDelay, FocusViewOptions } from '../views/nodes/FocusViewOptions';
 import { OpenWhere } from '../views/nodes/OpenWhere';
 import { PresBox } from '../views/nodes/trails';
 
@@ -352,21 +352,18 @@ export class DocumentManager {
             // if there's an options.effect, it will be handled from linkFollowHighlight.  We delay the start of
             // the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen
             // bcz: should this delay be an options parameter?
-            setTimeout(
-                () => {
-                    Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
-                    if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) {
-                        // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
-                        contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect);
-                        DocumentManager._overlayViews.add(contextView);
-                    }
-                    Doc.AddUnHighlightWatcher(() => {
-                        docView.Document[Animation] = undefined;
-                        DocumentManager.removeOverlayViews();
-                    });
-                },
-                (options.zoomTime ?? 0) * 0.5
-            );
+            setTimeout(() => {
+                Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
+                if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) {
+                    // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
+                    contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect);
+                    DocumentManager._overlayViews.add(contextView);
+                }
+                Doc.AddUnHighlightWatcher(() => {
+                    docView.Document[Animation] = undefined;
+                    DocumentManager.removeOverlayViews();
+                });
+            }, FocusEffectDelay(options));
             if (options.playMedia) docView.ComponentView?.playFrom?.(NumCast(docView.Document._layout_currentTimecode));
             if (options.playAudio) DocumentManager.playAudioAnno(docView.Document);
             if (options.toggleTarget && (!options.didMove || docView.Document.hidden)) docView.Document.hidden = !docView.Document.hidden;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5fd9223e3..a60415e43 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1043,13 +1043,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         >,
         root: Doc
     ) {
-        const dir = ((presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) || PresEffectDirection.Center) as PresEffectDirection;
+        const effectDirection = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection;
         const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null));
         const effectProps = {
-            left: dir === PresEffectDirection.Left,
-            right: dir === PresEffectDirection.Right,
-            top: dir === PresEffectDirection.Top,
-            bottom: dir === PresEffectDirection.Bottom,
+            left: effectDirection === PresEffectDirection.Left,
+            right: effectDirection === PresEffectDirection.Right,
+            top: effectDirection === PresEffectDirection.Top,
+            bottom: effectDirection === PresEffectDirection.Bottom,
             opposite: true,
             delay: 0,
             duration,
@@ -1063,7 +1063,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect));
         switch (presEffect) {
             case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce:    
-            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
+            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={effectDirection || PresEffectDirection.Left} presEffect={presEffect}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
             // case PresEffect.Fade:    return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
             case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>
             // keep as preset, doesn't really make sense with spring config
diff --git a/src/client/views/nodes/FocusViewOptions.ts b/src/client/views/nodes/FocusViewOptions.ts
index bb0d2b03c..1c462e98f 100644
--- a/src/client/views/nodes/FocusViewOptions.ts
+++ b/src/client/views/nodes/FocusViewOptions.ts
@@ -22,3 +22,14 @@ export interface FocusViewOptions {
     pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode)
     contextPath?: Doc[]; // path of inner documents that will also be focused
 }
+
+/**
+ * if there's an options.effect, it will be handled from linkFollowHighlight.  We delay the start of
+ * the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen
+ *  bcz: should this delay be an options parameter?
+ * @param options
+ * @returns
+ */
+export function FocusEffectDelay(options: FocusViewOptions) {
+    return (options.zoomTime ?? 0) * 0.5;
+}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index f23b32a48..7ca0bbc8f 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,7 +1,8 @@
+import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from '@dash/components';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import Slider from '@mui/material/Slider';
-import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from '@dash/components';
+import _ from 'lodash';
 import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -40,7 +41,7 @@ import { CollectionFreeFormPannableContents } from '../../collections/collection
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
-import { FocusViewOptions } from '../FocusViewOptions';
+import { FocusEffectDelay, FocusViewOptions } from '../FocusViewOptions';
 import { OpenWhere, OpenWhereMod } from '../OpenWhere';
 import { ScriptingBox } from '../ScriptingBox';
 import CubicBezierEditor, { EaseFuncToPoints, TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor';
@@ -48,7 +49,6 @@ import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import SlideEffect from './SlideEffect';
 import { AnimationSettings, SpringSettings, SpringType, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors } from './SpringUtils';
-import _ from 'lodash';
 
 @observer
 export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -547,8 +547,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array
             this.turnOffEdit();
-            this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list
-            this.doHideBeforeAfter(); // Handles hide after/before
+            this.navigateToActiveItem((options: FocusViewOptions) => {
+                setTimeout(this.doHideBeforeAfter, FocusEffectDelay(options)); // Handles hide after/before
+                finished?.();
+            }); // Handles movement to element only when presentationTrail is list
         }
     });
     static pinDataTypes(target?: Doc): dataTypes {
@@ -784,11 +786,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 });
             setTimeout(
                 () =>
-                    Array.from(transitioned).forEach(
-                        action(doc => {
-                            doc._dataTransition = undefined;
-                        })
-                    ),
+                    Array.from(transitioned).forEach(doc => {
+                        doc._dataTransition = undefined;
+                    }),
                 transTime + 10
             );
         }
@@ -826,16 +826,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * a new tab. If presCollection is undefined it will open the document
      * on the right.
      */
-    navigateToActiveItem = (afterNav?: () => void) => {
+    navigateToActiveItem = (afterNav?: (options: FocusViewOptions) => void) => {
         const { activeItem, targetDoc } = this;
-        const finished = () => {
-            afterNav?.();
+        const finished = (options: FocusViewOptions) => {
+            afterNav?.(options);
             targetDoc[Animation] = undefined;
         };
         const selViewCache = Array.from(this.selectedArray);
         const dragViewCache = Array.from(this._dragArray);
         const eleViewCache = Array.from(this._eleArray);
-        const resetSelection = action(() => {
+        const resetSelection = action((options: FocusViewOptions) => {
             if (!this._props.isSelected()) {
                 const presDocView = DocumentView.getDocumentView(this.Document);
                 if (presDocView) DocumentView.SelectView(presDocView, false);
@@ -844,14 +844,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
                 this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
             }
-            finished();
+            finished(options);
         });
         PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
     };
 
     public static PanelName = 'PRESBOX';
 
-    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
+    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) {
         if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
             return;
@@ -886,9 +886,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
                     DocumentView.SetLightboxDoc(undefined);
                 }
-                DocumentView.showDocument(targetDoc, options, finished);
+                DocumentView.showDocument(targetDoc, options, () => finished?.(options));
             });
-        } else finished?.();
+        } else finished?.(options);
     }
 
     /**
@@ -900,8 +900,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.childDocs.forEach((doc, index) => {
             const curDoc = Cast(doc, Doc, null);
             const tagDoc = PresBox.targetRenderedDoc(curDoc);
-            const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, curDoc);
-            let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
+            const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc);
+            let opacity = index === this.itemIndex ? 1 : undefined;
             if (curDoc.presentation_hide) {
                 if (index !== this.itemIndex) {
                     opacity = 1;
@@ -913,9 +913,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     opacity = 0;
                 } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) {
                     opacity = 1;
-                    setTimeout(() => {
-                        tagDoc._dataTransition = undefined;
-                    }, 1000);
                 }
             }
             const hidingIndAft =
-- 
cgit v1.2.3-70-g09d2


From deb038e6b659a1c7563ccc9df2ef51221c0492ea Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 3 Feb 2025 14:42:42 -0500
Subject: fixed pres animation of colection that's progresivized by removing
 dataTransition assignments that made no sense.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 26 ++++++++------------
 src/client/views/nodes/trails/PresBox.tsx          | 28 ++++++++++------------
 4 files changed, 25 insertions(+), 33 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 69fdf52ff..3c31b584e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -232,7 +232,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
-        return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true);
+        return DocumentView.SetViewTransition(docs, 'all', duration, timer, true);
     }
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 68105f2f1..beea6ab3c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -198,7 +198,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
-        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true);
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, true);
         const timecode = Math.round(time);
         docs.forEach(doc => {
             this.animFields.forEach(val => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a60415e43..0193fd328 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1390,8 +1390,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         this.Document[Animation] = presEffect;
         this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore
     };
-    public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
-        this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans);
+    public setViewTransition = (transProp: string, timeInMs: number, dataTrans = false) => {
+        this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, dataTrans);
     };
 
     public setCustomView = undoable((custom: boolean, layout: string): void => {
@@ -1590,21 +1590,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         );
     }
 
-    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, afterTrans?: () => void, dataTrans = false) {
-        docs.forEach(doc => {
-            doc._viewTransition = `${transProp} ${timeInMs}ms`;
-            dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
-        });
+    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, dataTrans = false) {
+        const setTrans = (transition?: string) =>
+            docs.forEach(doc => {
+                doc._viewTransition = transition;
+                dataTrans && (doc.dataTransition = transition);
+            });
+        setTrans(`${transProp} ${timeInMs}ms`);
         timer && clearTimeout(timer);
-        return setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    dataTrans && (doc.dataTransition = 'inherit');
-                    afterTrans?.();
-                }),
-            timeInMs + 10
-        );
+        return setTimeout(setTrans, timeInMs + 10);
     }
 
     // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 7ca0bbc8f..68634076d 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -452,27 +452,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const progressiveReveal = (first: boolean) => {
             const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null);
             if (presIndexed !== undefined) {
-                const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
-                targetRenderedDoc._dataTransition = 'all 1s';
-                targetRenderedDoc.opacity = 1;
-                setTimeout(() => {
-                    targetRenderedDoc._dataTransition = 'inherit';
-                }, 1000);
                 const listItems = this.progressivizedItems(this.activeItem);
-                if (listItems && presIndexed < listItems.length) {
+                const listItemDoc = listItems?.[presIndexed];
+                if (listItems && listItemDoc) {
                     if (!first) {
-                        const listItemDoc = listItems[presIndexed];
-                        const targetView = listItems && DocumentView.getFirstDocumentView(listItemDoc);
+                        const presBulletTiming = 500; // bcz: hardwired for now
                         Doc.linkFollowUnhighlight();
                         Doc.HighlightDoc(listItemDoc);
                         listItemDoc.presentation_effect = this.activeItem.presBulletEffect;
-                        listItemDoc.presentation_transition = 500;
-                        targetView?.setAnimEffect(listItemDoc, 500);
-                        if (targetView && this.activeItem.presBulletExpand) {
-                            targetView.setAnimateScaling(1.2, 400);
-                            Doc.AddUnHighlightWatcher(() => targetView?.setAnimateScaling(0, undefined));
-                        }
+                        listItemDoc.presentation_transition = presBulletTiming;
                         listItemDoc.opacity = undefined;
+
+                        const targetView = DocumentView.getFirstDocumentView(listItemDoc);
+                        if (targetView) {
+                            targetView.setAnimEffect(listItemDoc, presBulletTiming);
+                            if (this.activeItem.presBulletExpand) {
+                                targetView.setAnimateScaling(1.2, presBulletTiming * 0.8);
+                                Doc.AddUnHighlightWatcher(() => targetView.setAnimateScaling(0, undefined));
+                            }
+                        }
                         this.activeItem.presentation_indexed = presIndexed + 1;
                     }
                     return true;
-- 
cgit v1.2.3-70-g09d2

