aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--eslint.config.mjs2
-rw-r--r--package-lock.json128
-rw-r--r--package.json6
-rw-r--r--src/Utils.ts3
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/util/CurrentUserUtils.ts10
-rw-r--r--src/client/views/ContextMenu.tsx2
-rw-r--r--src/client/views/DocumentDecorations.tsx13
-rw-r--r--src/client/views/GestureOverlay.tsx319
-rw-r--r--src/client/views/InkTranscription.scss8
-rw-r--r--src/client/views/InkTranscription.tsx94
-rw-r--r--src/client/views/InkingStroke.tsx7
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx85
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx35
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx9
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx33
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx2
-rw-r--r--src/client/views/nodes/DiagramBox.scss4
-rw-r--r--src/client/views/nodes/DiagramBox.tsx19
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx3
-rw-r--r--src/client/views/nodes/IconTagBox.tsx2
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx2
-rw-r--r--src/fields/InkField.ts20
-rw-r--r--src/pen-gestures/GestureTypes.ts1
-rw-r--r--src/pen-gestures/ndollar.ts70
25 files changed, 526 insertions, 355 deletions
diff --git a/eslint.config.mjs b/eslint.config.mjs
index aebdc20d0..f7063caa5 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -48,7 +48,7 @@ export default [
'no-return-assign': 'error',
'no-await-in-loop': 'error',
'no-loop-func': 'error',
- '@typescript-eslint/no-cond-assign': 'error',
+ 'no-cond-assign': 'error',
'no-use-before-define': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'no-restricted-globals': ['error', 'event'],
diff --git a/package-lock.json b/package-lock.json
index a393ff5dc..178dec74e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,7 +51,7 @@
"@types/reveal": "^4.2.0",
"@types/supercluster": "^7.1.3",
"@types/textfit": "^2.4.4",
- "@types/web": "^0.0.163",
+ "@types/web": "^0.0.167",
"@types/webpack-hot-middleware": "^2.25.9",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"adm-zip": "^0.5.10",
@@ -70,7 +70,7 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.3.2",
"brotli": "^1.3.3",
- "browndash-components": "^0.1.47",
+ "browndash-components": "^0.1.49",
"browser-assert": "^1.2.1",
"bson": "^6.2.0",
"canvas": "^2.11.2",
@@ -269,7 +269,7 @@
"@types/d3": "^7.4.3",
"@types/dom-mediacapture-record": "^1.0.19",
"@types/exif": "^0.6.5",
- "@types/express": "^4.17.21",
+ "@types/express": "^5.0.0",
"@types/express-session": "^1.17.10",
"@types/file-saver": "^2.0.7",
"@types/howler": "^2.2.11",
@@ -9452,21 +9452,21 @@
}
},
"node_modules/@types/express": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
- "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
+ "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
- "@types/express-serve-static-core": "^4.17.33",
+ "@types/express-serve-static-core": "^5.0.0",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
- "version": "4.19.5",
- "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz",
- "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz",
+ "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -10102,9 +10102,9 @@
"dev": true
},
"node_modules/@types/web": {
- "version": "0.0.163",
- "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.163.tgz",
- "integrity": "sha512-5Pg2gKfulo186wFnv+YXx0luJGWQ94cCY2/Dy8lU5WAE50FdBoOK45uBbp8FceOSpLJ4UW3dmTW5tvsN9uuX7A=="
+ "version": "0.0.167",
+ "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.167.tgz",
+ "integrity": "sha512-IhdCUXL+vwofU/IzuOD+/T8Xteuk8fS9TvVJi7y7HTuURs7pWf3Kysxz4+lU0NSkj2LjDQ8AbKaaFFoAgeqYPg=="
},
"node_modules/@types/webgl-ext": {
"version": "0.0.30",
@@ -11660,9 +11660,9 @@
}
},
"node_modules/browndash-components": {
- "version": "0.1.47",
- "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.47.tgz",
- "integrity": "sha512-562nWku81I1wccgEQbMU/30+RP/81sxsIGoPGLH7Otm9TdrHXWgW8yJS4tzBPXBF03Q2AX8DyQusi6T0LSc7Mw==",
+ "version": "0.1.49",
+ "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.49.tgz",
+ "integrity": "sha512-XSW7XLSIml4qhDCHROzVTCzNgd878ndNQU8zC/M+UZqTyFCDLPGV5eo546IZ8QqEFOjUCp7jmzJS6jUcOeQ4HQ==",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
@@ -20465,42 +20465,6 @@
"node": ">= 14"
}
},
- "node_modules/http-proxy-middleware": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
- "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
- "dev": true,
- "dependencies": {
- "@types/http-proxy": "^1.17.8",
- "http-proxy": "^1.18.1",
- "is-glob": "^4.0.1",
- "is-plain-obj": "^3.0.0",
- "micromatch": "^4.0.2"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "@types/express": "^4.17.13"
- },
- "peerDependenciesMeta": {
- "@types/express": {
- "optional": true
- }
- }
- },
- "node_modules/http-proxy-middleware/node_modules/is-plain-obj": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
- "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -34004,6 +33968,54 @@
}
}
},
+ "node_modules/webpack-dev-server/node_modules/@types/express": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": {
+ "version": "4.19.6",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+ "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
+ "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
"node_modules/webpack-dev-server/node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -34013,6 +34025,18 @@
"node": ">= 10"
}
},
+ "node_modules/webpack-dev-server/node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/webpack-hot-middleware": {
"version": "2.26.1",
"resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz",
diff --git a/package.json b/package.json
index 20fa219b3..e356baa9f 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"@types/d3": "^7.4.3",
"@types/dom-mediacapture-record": "^1.0.19",
"@types/exif": "^0.6.5",
- "@types/express": "^4.17.21",
+ "@types/express": "^5.0.0",
"@types/express-session": "^1.17.10",
"@types/file-saver": "^2.0.7",
"@types/howler": "^2.2.11",
@@ -130,7 +130,7 @@
"@types/reveal": "^4.2.0",
"@types/supercluster": "^7.1.3",
"@types/textfit": "^2.4.4",
- "@types/web": "^0.0.163",
+ "@types/web": "^0.0.167",
"@types/webpack-hot-middleware": "^2.25.9",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"adm-zip": "^0.5.10",
@@ -149,7 +149,7 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.3.2",
"brotli": "^1.3.3",
- "browndash-components": "^0.1.47",
+ "browndash-components": "^0.1.49",
"browser-assert": "^1.2.1",
"bson": "^6.2.0",
"canvas": "^2.11.2",
diff --git a/src/Utils.ts b/src/Utils.ts
index 0590c6930..724725c23 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-namespace */
import * as uuid from 'uuid';
export function clamp(n: number, lower: number, upper: number) {
@@ -205,7 +204,6 @@ export function intersectRect(r1: { left: number; top: number; width: number; he
}
export function stringHash(s?: string) {
- // eslint-disable-next-line no-bitwise
return !s ? undefined : Math.abs(s.split('').reduce((a, b) => (n => n & n)((a << 5) - a + b.charCodeAt(0)), 0));
}
@@ -254,6 +252,7 @@ export namespace JSONUtils {
try {
results = JSON.parse(source);
} catch (e) {
+ console.log('JSONparse error: ', e);
results = source;
}
return results;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ba1bc8b7a..bf72a4bd4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -319,11 +319,11 @@ export class DocumentOptions {
contextMenuLabels?: List<string>;
contextMenuIcons?: List<string>;
childContentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
- childFilters_boolean?: string;
+ childFilters_boolean?: STRt = new StrInfo('boolean operator to apply to filters on different metadata fields. Value should be AND or OR. Default is AND');
childFilters?: List<string>;
childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width', false);
childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view)
- childLayoutString?: string; // template string for collection to use to render its children
+ childLayoutString?: STRt = new StrInfo('JSX layout string for rendering children of a (collection) Doc'); // template string for collection to use to render its children
childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active');
childLayoutFitWidth?: BOOLt = new BoolInfo("whether a child doc's fitWith should be overriden by collection");
childDontRegisterViews?: BOOLt = new BoolInfo('whether child document views should be registered so that they can be found when following links, etc');
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f296d26bd..05f5621a3 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -743,7 +743,7 @@ pie title Minerals in my tap water
{ 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_);}'} },
{ title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
- { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
+ { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment",ignoreClick: true,
subMenu: [
{ title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -806,7 +806,7 @@ pie title Minerals in my tap water
{ title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }},
];
}
- static contextMenuTools():Button[] {
+ static contextMenuTools(doc:Doc):Button[] {
return [
{ btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
@@ -823,6 +823,7 @@ pie title Minerals in my tap water
{ 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: "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},
{ 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
{ title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
{ title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
@@ -835,8 +836,7 @@ pie title Minerals in my tap water
{ title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
{ title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
{ title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected
- { 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: 150},
- ];
+ ];
}
/// initializes a context menu button for the top bar context menu
@@ -881,7 +881,7 @@ pie title Minerals in my tap water
static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
- const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
+ const ctxtMenuBtns = CurrentUserUtils.contextMenuTools(doc).map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
}
/// Initializes all the default buttons for the top bar context menu
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 5edb5fc0d..399e8a238 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -252,7 +252,7 @@ export class ContextMenu extends ObservableReactComponent<{ noexpand?: boolean }
this._selectedIndex--;
}
e.preventDefault();
- } else if (e.key === 'Enter' || e.key === 'Tab') {
+ } else if ((e.key === 'Enter' || e.key === 'Tab') && this._selectedIndex >= 0) {
const item = this.flatItems[this._selectedIndex];
if (item.event) {
item.event({ x: this.pageX, y: this.pageY });
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 7f6cfbb87..df9ec1bbf 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { IconButton } from 'browndash-components';
-import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaUndo } from 'react-icons/fa';
@@ -59,6 +59,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
private _interactionLock?: boolean;
@observable _showNothing = true;
+ @observable private _forceRender = 0;
@observable private _accumulatedTitle = '';
@observable private _titleControlString: string = '$title';
@observable private _editingTitle = false;
@@ -625,8 +626,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
});
};
- @computed
- get selectionTitle(): string {
+ @computed get selectionTitle(): string {
if (DocumentView.Selected().length === 1) {
const selected = DocumentView.Selected()[0];
if (this._titleControlString.startsWith('$')) {
@@ -647,8 +647,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
}
return this._rotCenter;
}
-
render() {
+ this._forceRender;
const { b, r, x, y } = this.Bounds;
const seldocview = DocumentView.Selected().lastElement();
if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
@@ -661,6 +661,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
return null;
}
+ if (seldocview && !seldocview?.ContentDiv?.getBoundingClientRect().width) {
+ setTimeout(action(() => this._forceRender++)); // if the selected Doc has no width, then assume it's stil being layed out and try to render again later.
+ return null;
+ }
+
// sharing
const acl = GetEffectiveAcl(!this._showLayoutAcl ? Doc.GetProto(seldocview.Document) : seldocview.Document);
const docShareMode = HierarchyMapping.get(acl)!.name;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index befd19f6e..5fddaec9a 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,34 +3,40 @@ 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 { emptyFunction } from '../../Utils';
+import { emptyFunction, intersectRect } from '../../Utils';
import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { NumCast } from '../../fields/Types';
+import { Gestures } from '../../pen-gestures/GestureTypes';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+import { Result } from '../../pen-gestures/ndollar';
+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 { 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,
ActiveInkColor,
ActiveInkWidth,
+ DocumentView,
SetActiveArrowStart,
SetActiveDash,
SetActiveFillColor,
SetActiveInkColor,
SetActiveInkWidth,
} from './nodes/DocumentView';
-import { Gestures } from '../../pen-gestures/GestureTypes';
-import { GestureUtils } from '../../pen-gestures/GestureUtils';
-import { InteractionUtils } from '../util/InteractionUtils';
-import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
-import './GestureOverlay.scss';
-import { ObservableReactComponent } from './ObservableReactComponent';
-import { returnEmptyDocViewList } from './StyleProvider';
-import { ActiveFillColor, DocumentView } from './nodes/DocumentView';
-
export enum ToolglassTools {
InkToText = 'inktotext',
IgnoreGesture = 'ignoregesture',
@@ -41,6 +47,10 @@ interface GestureOverlayProps {
isActive: boolean;
}
@observer
+/**
+ * class for gestures. will determine if what the user drew is a gesture, and will transform the ink stroke into the shape the user
+ * drew or perform the gesture's action
+ */
export class GestureOverlay extends ObservableReactComponent<React.PropsWithChildren<GestureOverlayProps>> {
// eslint-disable-next-line no-use-before-define
static Instance: GestureOverlay;
@@ -70,10 +80,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
private _overlayRef = React.createRef<HTMLDivElement>();
- private _d1: Doc | undefined;
- private _inkToTextDoc: Doc | undefined;
- private thumbIdentifier?: number;
- private pointerIdentifier?: number;
constructor(props: GestureOverlayProps) {
super(props);
@@ -88,7 +94,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
componentDidMount() {
GestureOverlay.Instance = this;
}
-
@action
onPointerDown = (e: React.PointerEvent) => {
if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) {
@@ -127,78 +132,241 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
// SetActiveArrowEnd('none');
}
}
+ /**
+ * If what the user drew is a scribble, this returns the documents that were scribbled over
+ * I changed it so it doesnt use triangles. It will modify an intersect array, with its length being
+ * how many sharp cusps there are. The first index will have a boolean that is based on if there is an
+ * intersection in the first 1/length percent of the stroke. The second index will be if there is an intersection
+ * in the 2nd 1/length percent of the stroke. This array will be used in determineIfScribble().
+ * @param ffview freeform view where scribble is drawn
+ * @param scribbleStroke scribble stroke in screen space coordinats
+ * @returns array of documents scribbled over
+ */
+ isScribble = (ffView: CollectionFreeFormView, cuspArray: { X: number; Y: number }[], scribbleStroke: { X: number; Y: number }[]) => {
+ const intersectArray = cuspArray.map(() => false);
+ const scribbleBounds = InkField.getBounds(scribbleStroke);
+ const docsToDelete = ffView.childDocs
+ .map(doc => DocumentView.getDocumentView(doc))
+ .filter(dv => dv?.ComponentView instanceof InkingStroke)
+ .map(dv => dv?.ComponentView as InkingStroke)
+ .filter(otherInk => {
+ const otherScreenPts = otherInk.inkScaledData?.().inkData.map(otherInk.ptToScreen);
+ if (intersectRect(InkField.getBounds(otherScreenPts), scribbleBounds)) {
+ const intersects = this.findInkIntersections(scribbleStroke, otherScreenPts).map(intersect => {
+ const percentage = intersect.split('/')[0];
+ intersectArray[Math.floor(Number(percentage) * cuspArray.length)] = true;
+ });
+ return intersects.length > 0;
+ }
+ });
+ return !this.determineIfScribble(intersectArray) ? undefined :
+ [ ...docsToDelete.map(stroke => stroke.Document),
+ // bcz: NOTE: docsInBoundingBox test should be replaced with a docsInConvexHull test
+ ...this.docsInBoundingBox({ topLeft : ffView.ScreenToContentsXf().transformPoint(scribbleBounds.left, scribbleBounds.top),
+ bottomRight: ffView.ScreenToContentsXf().transformPoint(scribbleBounds.right,scribbleBounds.bottom)},
+ ffView.childDocs.filter(doc => !docsToDelete.map(s => s.Document).includes(doc)) )]; // prettier-ignore
+ };
+ /**
+ * Returns all docs in array that overlap bounds. Note that the bounds should be given in screen space coordinates.
+ * @param boundingBox screen space bounding box
+ * @param childDocs array of docs to test against bounding box
+ * @returns list of docs that overlap rect
+ */
+ docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => {
+ const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] };
+ return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
+ };
+ /**
+ * 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
+ * @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs
+ * @returns
+ */
+ 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
+ const { start, end } = intersectArray.reduce((res, val, i) => ({ // test for scribbles at start and end of scribble stroke
+ start: res.start || (val && i <= quarterArrayLength),
+ end: res.end || (val && i >= intersectArray.length - quarterArrayLength)
+ }), { start: false, end: false }); // prettier-ignore
+
+ const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length;
+ return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end));
+ };
+ /**
+ * determines if inks intersect
+ * @param line is pointData
+ * @param triangle triangle with 3 points
+ * @returns will return an array, with its lenght being equal to how many intersections there are betweent the 2 strokes.
+ * each item in the array will contain a number between 0-1 or a number 0-1 seperated by a comma. If one of the curves is a line, then
+ * then there will just be a number that reprents how far that intersection is along the scribble. For example,
+ * .1 means that the intersection occurs 10% into the scribble, so near the beginning of it. but if they are both curves, then
+ * it will return two numbers, one for each curve, seperated by a comma. Sometimes, the percentage it returns is inaccurate,
+ * espcially in the beginning and end parts of the stroke. dont know why. hope this makes sense
+ */
+ findInkIntersections = (scribble: InkData, inkStroke: InkData): string[] => {
+ const intersectArray: string[] = [];
+ const scribbleBounds = InkField.getBounds(scribble);
+ for (let i = 0; i < scribble.length - 3; i += 4) { // for each segment of scribble
+ for (let j = 0; j < inkStroke.length - 3; j += 4) { // for each segment of ink stroke
+ const scribbleSeg = InkField.Segment(scribble, i);
+ const strokeSeg = InkField.Segment(inkStroke, j);
+ const strokeBounds = InkField.getBounds(strokeSeg.points.map(pt => ({ X: pt.x, Y: pt.y })));
+ if (intersectRect(scribbleBounds, strokeBounds)) {
+ const result = InkField.bintersects(scribbleSeg, strokeSeg)[0];
+ if (result !== undefined) {
+ intersectArray.push(result.toString());
+ }
+ }
+ } // prettier-ignore
+ } // prettier-ignore
+ return intersectArray;
+ };
+ dryInk = () => {
+ const newPoints = this._points.reduce((p, pts) => {
+ p.push([pts.X, pts.Y]);
+ return p;
+ }, [] as number[][]);
+ newPoints.pop();
+ const controlPoints: { X: number; Y: number }[] = [];
+
+ const bezierCurves = fitCurve.default(newPoints, 10);
+ Array.from(bezierCurves).forEach(curve => {
+ controlPoints.push({ X: curve[0][0], Y: curve[0][1] });
+ controlPoints.push({ X: curve[1][0], Y: curve[1][1] });
+ controlPoints.push({ X: curve[2][0], Y: curve[2][1] });
+ controlPoints.push({ X: curve[3][0], Y: curve[3][1] });
+ });
+ const dist = Math.sqrt((controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y));
+ if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
+ this._points.length = 0;
+ this._points.push(...controlPoints);
+ this.dispatchGesture(Gestures.Stroke);
+ };
@action
onPointerUp = () => {
+ const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
DocumentView.DownDocView = 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 }));
-
+ const { Name, Score } =
+ (this.InkShape
+ ? new Result(this.InkShape, 1, Date.now)
+ : Doc.UserDoc().recognizeGestures && points.length > 2
+ ? GestureUtils.GestureRecognizer.Recognize([points])
+ : undefined) ??
+ new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore
+
+ const cuspArray = this.getCusps(points);
// if any of the shape is activated in the CollectionFreeFormViewChrome
- if (this.InkShape) {
- GestureOverlay.makeBezierPolygon(this._points, this.InkShape, false);
- this.dispatchGesture(this.InkShape);
- this.primCreated();
- }
- // if we're not drawing in a toolglass try to recognize as gesture
- else {
- // need to decide when to turn gestures back on
- const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]);
- let actionPerformed = false;
- if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
- switch (result.Name) {
- case Gestures.Line:
- case Gestures.Triangle:
- case Gestures.Rectangle:
- case Gestures.Circle:
- GestureOverlay.makeBezierPolygon(this._points, result.Name, true);
- actionPerformed = this.dispatchGesture(result.Name);
- break;
- case Gestures.Scribble:
- console.log('scribble');
- break;
- case Gestures.RightAngle:
- console.log('RightAngle');
- break;
- default: {
- /* empty */
- }
- }
+ // need to decide when to turn gestures back on
+ const actionPerformed = ((name: Gestures) => {
+ switch (name) {
+ case Gestures.Line:
+ if (cuspArray.length > 2) return undefined;
+ // eslint-disable-next-line no-fallthrough
+ case Gestures.Triangle:
+ case Gestures.Rectangle:
+ case Gestures.Circle:
+ this.makeBezierPolygon(this._points, Name, true);
+ return this.dispatchGesture(name);
+ case Gestures.RightAngle:
+ return ffView && this.convertToText(ffView).length > 0;
+ default:
}
-
- // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document
- if (!actionPerformed) {
- const newPoints = this._points.reduce((p, pts) => {
- p.push([pts.X, pts.Y]);
- return p;
- }, [] as number[][]);
- newPoints.pop();
- const controlPoints: { X: number; Y: number }[] = [];
-
- const bezierCurves = fitCurve.default(newPoints, 10);
- Array.from(bezierCurves).forEach(curve => {
- controlPoints.push({ X: curve[0][0], Y: curve[0][1] });
- controlPoints.push({ X: curve[1][0], Y: curve[1][1] });
- controlPoints.push({ X: curve[2][0], Y: curve[2][1] });
- controlPoints.push({ X: curve[3][0], Y: curve[3][1] });
- });
- const dist = Math.sqrt(
- (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)
- );
- // eslint-disable-next-line prefer-destructuring
- if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
- this._points.length = 0;
- this._points.push(...controlPoints);
- this.dispatchGesture(Gestures.Stroke);
+ })(Score < 0.7 ? Gestures.Stroke : (Name as Gestures));
+ // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document
+
+ if (!actionPerformed) {
+ const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points);
+ if (scribbledOver) {
+ undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')();
+ } else {
+ this.dryInk();
}
}
}
this._points.length = 0;
};
-
- public static makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => {
- const xs = points.map(p => p.X);
- const ys = points.map(p => p.Y);
+ /**
+ * used in the rightAngle gesture to convert handwriting into text. will only work on collections
+ * TODO: make it work on individual ink docs.
+ */
+ convertToText = (ffView: CollectionFreeFormView) => {
+ let minX = 999999999;
+ let maxX = -999999999;
+ let minY = 999999999;
+ let maxY = -999999999;
+ const textDocs: Doc[] = [];
+ ffView.childDocs
+ .filter(doc => doc.type === DocumentType.COL)
+ .forEach(doc => {
+ if (typeof doc.width === 'number' && typeof doc.height === 'number' && typeof doc.x === 'number' && typeof doc.y === 'number') {
+ const bounds = DocumentView.getDocumentView(doc)?.getBounds;
+ if (bounds) {
+ if (intersectRect({ ...bounds, width: bounds.right - bounds.left, height: bounds.bottom - bounds.top }, InkField.getBounds(this._points))) {
+ if (doc.x < minX) {
+ minX = doc.x;
+ }
+ if (doc.x > maxX) {
+ maxX = doc.x;
+ }
+ if (doc.y < minY) {
+ minY = doc.y;
+ }
+ if (doc.y + doc.height > maxY) {
+ maxY = doc.y + doc.height;
+ }
+ const newDoc = Docs.Create.TextDocument(doc.transcription as string, { title: '', x: doc.x as number, y: minY });
+ newDoc.height = doc.height;
+ newDoc.width = doc.width;
+ if (ffView.addDocument && ffView.removeDocument) {
+ ffView.addDocument(newDoc);
+ ffView.removeDocument(doc);
+ }
+ textDocs.push(newDoc);
+ }
+ }
+ }
+ });
+ return textDocs;
+ };
+ /**
+ * Returns array of coordinates corresponding to the sharp cusps in an input stroke
+ * @param points array of X,Y stroke coordinates
+ * @returns array containing the coordinates of the sharp cusps
+ */
+ getCusps(points: InkData) {
+ const arrayOfPoints: { X: number; Y: number }[] = [];
+ arrayOfPoints.push(points[0]);
+ for (let i = 0; i < points.length - 2; i++) {
+ const point1 = points[i];
+ const point2 = points[i + 1];
+ const point3 = points[i + 2];
+ 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);
+ }
+ }
+ arrayOfPoints.push(points[points.length - 1]);
+ return arrayOfPoints;
+ }
+ /**
+ * takes in three points and then determines the angle of the points. used to determine if the cusp
+ * is sharp enoug
+ * @returns
+ */
+ find_angle(A: { X: number; Y: number }, B: { X: number; Y: number }, C: { X: number; Y: number }) {
+ const AB = Math.sqrt(Math.pow(B.X - A.X, 2) + Math.pow(B.Y - A.Y, 2));
+ const BC = Math.sqrt(Math.pow(B.X - C.X, 2) + Math.pow(B.Y - C.Y, 2));
+ const AC = Math.sqrt(Math.pow(C.X - A.X, 2) + Math.pow(C.Y - A.Y, 2));
+ return Math.acos((BC * BC + AB * AB - AC * AC) / (2 * BC * AB)) * (180 / Math.PI);
+ }
+ makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => {
+ const xs = this._points.map(p => p.X);
+ const ys = this._points.map(p => p.Y);
let right = Math.max(...xs);
let left = Math.min(...xs);
let bottom = Math.max(...ys);
@@ -394,7 +562,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this._strokes.map((l, i) => {
const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
return (
- // eslint-disable-next-line react/no-array-index-key
<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,
diff --git a/src/client/views/InkTranscription.scss b/src/client/views/InkTranscription.scss
index 18d6b8b10..c77117ccc 100644
--- a/src/client/views/InkTranscription.scss
+++ b/src/client/views/InkTranscription.scss
@@ -2,9 +2,7 @@
.error-msg {
display: none !important;
}
- .ms-editor{
- .smartguide{
- top:1000px;
- }
+ .ms-editor {
+ top: 1000px;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 26e0aa372..8698a6962 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -3,7 +3,7 @@ import { action, observable } from 'mobx';
import * as React from 'react';
import { Doc, DocListCast } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
-import { Cast, DateCast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { Cast, DateCast, ImageCast, NumCast } from '../../fields/Types';
import { aggregateBounds } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm';
@@ -11,11 +11,8 @@ import { InkingStroke } from './InkingStroke';
import './InkTranscription.scss';
import { Docs } from '../documents/Documents';
import { DocumentView } from './nodes/DocumentView';
-import { Number } from 'mongoose';
-import { NumberArray } from 'd3';
import { ImageField } from '../../fields/URLField';
import { gptHandwriting } from '../apis/gpt/GPT';
-import * as fs from 'fs';
import { URLField } from '../../fields/URLField';
/**
* Class component that handles inking in writing mode
@@ -23,30 +20,30 @@ import { URLField } from '../../fields/URLField';
export class InkTranscription extends React.Component {
static Instance: InkTranscription;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
@observable _mathRegister: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
@observable _mathRef: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
@observable _textRegister: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
@observable _textRef: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
@observable iinkEditor: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
private lastJiix: any;
private currGroup?: Doc;
private collectionFreeForm?: CollectionFreeFormView;
- constructor(props: Readonly<{}>) {
+ constructor(props: Readonly<object>) {
super(props);
InkTranscription.Instance = this;
}
-
- componentWillUnmount() {
- // this._mathRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
- // this._textRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
- }
-
@action
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
setMathRef = async (r: any) => {
if (!this._textRegister && r) {
- let editor;
const options = {
configuration: {
server: {
@@ -61,21 +58,22 @@ export class InkTranscription extends React.Component {
},
},
};
-
- editor = new iink.Editor(r, options as any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const editor = new iink.Editor(r, options as any);
await editor.initialize();
this._textRegister = r;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
return (this._textRef = r);
}
};
@action
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
setTextRef = async (r: any) => {
if (!this._textRegister && r) {
- let editor;
const options = {
configuration: {
server: {
@@ -90,12 +88,13 @@ export class InkTranscription extends React.Component {
},
},
};
-
- editor = new iink.Editor(r, options as any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const editor = new iink.Editor(r, options as any);
await editor.initialize();
this.iinkEditor = editor;
this._textRegister = r;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
return (this._textRef = r);
@@ -124,28 +123,9 @@ export class InkTranscription extends React.Component {
strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
times.push(DateCast(i.author_date).getDate().getTime());
});
- console.log(strokes);
- console.log(
- `this.Multistrokes.push(
- new Multistroke(
- Gestures.Scribble,
- useBoundedRotationInvariance,
- new Array([
- ` +
- this.convertPointsToString(strokes) +
- `
- ])
- )
- )
- `
- );
- //console.log(this.convertPointsToString(strokes));
- //console.log(this.convertPointsToString2(strokes));
this.currGroup = groupDoc;
const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
- const processGestures = false;
if (math) {
- console.log('math');
this.iinkEditor.importPointEvents(pointerData);
} else {
this.iinkEditor.importPointEvents(pointerData);
@@ -172,9 +152,9 @@ export class InkTranscription extends React.Component {
t: number;
p: number;
}
- let strokeObjects: strokeData[] = [];
+ const strokeObjects: strokeData[] = [];
stroke.forEach(point => {
- let tempObject: strokeData = {
+ const tempObject: strokeData = {
x: point.X,
y: point.Y,
t: time,
@@ -220,6 +200,7 @@ export class InkTranscription extends React.Component {
* @param e the event objects
* @param ref the ref to the editor
*/
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
exportInk = async (e: any, ref: any) => {
const exports = e.detail['application/vnd.myscript.jiix'];
if (exports) {
@@ -236,6 +217,7 @@ export class InkTranscription extends React.Component {
this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']);
// map timestamp to strokes
const timestampWord = new Map<number, string>();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.lastJiix.words.map((word: any) => {
if (word.items) {
word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
@@ -271,12 +253,7 @@ export class InkTranscription extends React.Component {
if (this.currGroup && text) {
DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
- this.currGroup.transcription = text;
- this.currGroup.title = text;
- let image = await DocumentView.GetDocImage(this.currGroup);
- const pathname = image?.url.href as string;
- console.log(image?.url);
- console.log(image);
+ const image = await this.getIcon();
const { href } = (image as URLField).url;
const hrefParts = href.split('.');
const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
@@ -284,11 +261,12 @@ export class InkTranscription extends React.Component {
try {
const hrefBase64 = await this.imageUrlToBase64(hrefComplete);
response = await gptHandwriting(hrefBase64);
- console.log(response);
- } catch (error) {
- console.log('bad things have happened');
+ } catch {
+ console.error('Error getting image');
}
const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
+ this.currGroup.transcription = response;
+ this.currGroup.title = response;
if (!this.currGroup.hasTextBox) {
const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
newDoc.height = 200;
@@ -300,6 +278,22 @@ export class InkTranscription extends React.Component {
}
}
};
+ /**
+ * gets the icon of the collection that was just made
+ * @returns the image of the collection
+ */
+ async getIcon() {
+ const docView = DocumentView.getDocumentView(this.currGroup);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+ /**
+ * converts the image to base url formate
+ * @param imageUrl imageurl taken from the collection icon
+ */
imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
try {
const response = await fetch(imageUrl);
@@ -403,8 +397,8 @@ export class InkTranscription extends React.Component {
}
// nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
- newCollection && docView.props.addDocument?.(newCollection);
if (newCollection) {
+ docView.props.addDocument?.(newCollection);
newCollection.hasTextBox = false;
}
return newCollection;
@@ -413,8 +407,8 @@ export class InkTranscription extends React.Component {
render() {
return (
<div className="ink-transcription">
- <div className="math-editor" ref={this.setMathRef} touch-action="none"></div>
- <div className="text-editor" ref={this.setTextRef} touch-action="none"></div>
+ <div className="math-editor" ref={this.setMathRef}></div>
+ <div className="text-editor" ref={this.setTextRef}></div>
</div>
);
}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 32bbd57c6..177400882 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -49,7 +49,7 @@ import { PinDocView, PinProps } from './PinFuncs';
import { StyleProp } from './StyleProp';
import { ViewBoxInterface } from './ViewBoxInterface';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
+// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@observer
@@ -331,9 +331,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
1,
1,
'' as Gestures,
- 'none',
+ 'all',
1.0,
- false
+ false,
+ this.onPointerDown
)}
<InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={this.screenCtrlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
<InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index d9c27254a..e5a6ebc7f 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,4 +1,4 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
@@ -46,8 +46,9 @@ export class CollectionCardView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [key: string]: IReactionDisposer } = {};
private _textToDoc = new Map<string, Doc>();
+ private _dropped = false; // indicate when a card doc has just moved;
- @observable _forceChildXf = false;
+ @observable _forceChildXf = 0;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
@observable _maxRowCount = 10;
@@ -99,8 +100,7 @@ export class CollectionCardView extends CollectionSubView() {
};
componentDidMount() {
- this.Document.childFilters_boolean = 'OR'; // bcz: really shouldn't be assigning to fields from within didMount -- this should be a default/override beahavior somehow
-
+ this._props.setContentViewBox?.(this);
// Reaction to cardSort changes
this._disposers.sort = reaction(
() => GPTPopup.Instance.visible,
@@ -112,6 +112,17 @@ export class CollectionCardView extends CollectionSubView() {
}
}
);
+ // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
+ // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the
+ // dash data just changed and trigger a React involidation with the correct data (read from the dom).
+ this._disposers.child = reaction(
+ () => [this.Document.x, this.Document.y],
+ () => {
+ if (!Array.from(this._docRefs.values()).every(dv => dv.ContentDiv?.getBoundingClientRect().width)) {
+ setTimeout(action(() => this._forceChildXf++));
+ }
+ }
+ );
}
componentWillUnmount() {
@@ -176,6 +187,7 @@ export class CollectionCardView extends CollectionSubView() {
inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d));
childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
+ childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
isChildContentActive = () => !!this.isContentActive();
@@ -267,14 +279,7 @@ export class CollectionCardView extends CollectionSubView() {
@action
onPointerMove = (x: number, y: number) => {
- this._docDraggedIndex = -1;
- if (DragManager.docsBeingDragged.length) {
- const newIndex = this.findCardDropIndex(x, y);
-
- if (newIndex !== this._docDraggedIndex && newIndex != -1) {
- this._docDraggedIndex = newIndex;
- }
- }
+ this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1;
};
/**
@@ -306,6 +311,7 @@ export class CollectionCardView extends CollectionSubView() {
if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
this.dataDoc[this.fieldKey] = new List<Doc>(sorted);
}
+ this._dropped = true;
}
e.stopPropagation();
return true;
@@ -340,7 +346,8 @@ export class CollectionCardView extends CollectionSubView() {
* @param isDesc
* @returns
*/
- sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
+ sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
+ const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
sortType &&
docs.sort((docA, docB) => {
const [typeA, typeB] = (() => {
@@ -364,21 +371,21 @@ export class CollectionCardView extends CollectionSubView() {
const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
return isDesc ? out : -out;
});
- if (dragIndex != -1) {
+ if (dragIndex !== -1) {
const draggedDoc = DragManager.docsBeingDragged[0];
const originalIndex = docs.findIndex(doc => doc === draggedDoc);
originalIndex !== -1 && docs.splice(originalIndex, 1);
- docs.splice(dragIndex, 0, draggedDoc);
+ draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
}
- return [...docs]; // need to spread docs into a new object list since sort() modifies the incoming list which confuses mobx caching
+ return docs;
};
displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => (
<DocumentView
{...this._props}
- ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))}
+ ref={action((r: DocumentView) => (!r?.ContentDiv ? this._docRefs.delete(doc) : this._docRefs.set(doc, r)))}
Document={doc}
NativeWidth={returnZero}
NativeHeight={returnZero}
@@ -387,11 +394,12 @@ export class CollectionCardView extends CollectionSubView() {
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
+ containerViewPath={this.childContainerViewPath}
ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
isContentActive={emptyFunction}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.childPanelWidth}
- PanelHeight={() => this._props.PanelHeight() * this.fitContentScale}
+ PanelHeight={this.childPanelHeight}
dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
showTags={true}
@@ -593,7 +601,10 @@ export class CollectionCardView extends CollectionSubView() {
const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false;
const childScreenToLocal = () => {
+ // need to explicitly trigger an invalidation since we're reading everything from the Dom
this._forceChildXf;
+ this._props.ScreenToLocalTransform();
+
const dref = this._docRefs.get(doc);
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
return new Transform(-translateX + (dref?.centeringX || 0) * scale,
@@ -608,25 +619,30 @@ export class CollectionCardView extends CollectionSubView() {
return (rowCenterIndex - indexInRow) * 100 - 50;
};
const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
- const vscale = Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()),
- (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10))); // prettier-ignore
+ const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()),
+ (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
return (
<div
key={doc[Id]}
className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
- onPointerUp={() => {
- if (DocumentView.SelectedDocs().includes(doc)) return;
- // this turns off documentDecorations during a transition, then turns them back on afterward.
- SnappingManager.SetIsResizing(doc[Id]);
- setTimeout(
- action(() => {
- SnappingManager.SetIsResizing(undefined);
- this._forceChildXf = !this._forceChildXf;
- }),
- 900
- );
- }}
+ onPointerUp={action(() => {
+ // if a card doc has just moved, or a card is selected and in front, then ignore this event
+ if (DocumentView.SelectedDocs().includes(doc) || this._dropped) {
+ this._dropped = false;
+ } else {
+ // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts.
+ // Turn them back on when the animation has completed and the render and backend structures are in synch
+ SnappingManager.SetIsResizing(doc[Id]);
+ setTimeout(
+ action(() => {
+ SnappingManager.SetIsResizing(undefined);
+ this._forceChildXf++;
+ }),
+ 600
+ );
+ }
+ })}
style={{
width: this.childPanelWidth(),
height: 'max-content',
@@ -635,7 +651,7 @@ export class CollectionCardView extends CollectionSubView() {
rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`,
}} // prettier-ignore
- onMouseEnter={() => this.setHoveredNodeIndex(index)}>
+ onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}>
{this.displayDoc(doc, childScreenToLocal)}
</div>
);
@@ -649,7 +665,8 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="collectionCardView-outer"
ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
- onPointerMove={e => this.onPointerMove(e.clientX, e.clientY)}
+ onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+ onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
onDrop={this.onExternalDrop.bind(this)}
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 4951590dc..cf86a0a4e 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -14,6 +14,7 @@ import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
import './CollectionCarousel3DView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { Transform } from '../../util/Transform';
const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -45,15 +46,32 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
centerScale = Number(CAROUSEL3D_CENTER_SCALE);
+ sideScale = Number(CAROUSEL3D_SIDE_SCALE);
panelWidth = () => this._props.PanelWidth() / 3;
- panelHeight = () => this._props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE);
+ panelHeight = () => this._props.PanelHeight() * this.sideScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
isChildContentActive = () => !!this.isContentActive();
- childScreenToLocal = () =>
- this._props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1)
- .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor.
- .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2)
+ childScreenLeftToLocal = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .scale(this._props.NativeDimScaling?.() || 1)
+ .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .scale(1 / this.sideScale);
+ childScreenRightToLocal = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .scale(this._props.NativeDimScaling?.() || 1)
+ .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .scale(1 / this.sideScale);
+ childCenterScreenToLocal = () =>
+ this._props
+ .ScreenToLocalTransform()
+ .scale(this._props.NativeDimScaling?.() || 1)
+ .translate(
+ -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
+ -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
+ ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2
.scale(1 / this.centerScale);
focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
@@ -65,9 +83,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
index !== -1 && (this.layoutDoc._carousel_index = index);
return undefined;
};
+
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._carousel_index);
- const displayDoc = (childPair: { layout: Doc; data: Doc }) => (
+ const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => (
<DocumentView
// eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
@@ -83,7 +102,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
focus={this.focus}
- ScreenToLocalTransform={this.childScreenToLocal}
+ ScreenToLocalTransform={dxf}
isContentActive={this.isChildContentActive}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
@@ -93,7 +112,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return this.carouselItems.map((childPair, index) => (
<div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
- {displayDoc(childPair)}
+ {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)}
</div>
));
}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 9741c45fe..143cfe0e8 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -71,7 +71,7 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
@computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
@computed get carouselItems() {
- return DocListCast(this.childDocList)
+ return this.childDocs
.filter(doc => doc.type !== DocumentType.LINK)
.filter(doc => {
switch (StrCast(this.layoutDoc.filterOp)) {
@@ -155,6 +155,8 @@ export class CollectionCarouselView extends CollectionSubView() {
? false
: undefined;
+ childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+
renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
return (
<DocumentView
@@ -164,8 +166,10 @@ export class CollectionCarouselView extends CollectionSubView() {
NativeWidth={returnZero}
NativeHeight={returnZero}
fitWidth={undefined}
+ showTags={true}
containerViewPath={this.childContainerViewPath}
setContentViewBox={undefined}
+ ScreenToLocalTransform={this.childScreenToLocalXf}
onDoubleClickScript={this.onContentDoubleClick}
onClickScript={this.onContentClick}
isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -245,9 +249,6 @@ export class CollectionCarouselView extends CollectionSubView() {
<div key="fwd" className="carouselView-fwd" onClick={this.advance}>
<FontAwesomeIcon icon="chevron-right" size="2x" />
</div>
- <div key="star" className="carouselView-star" onClick={this.star}>
- <FontAwesomeIcon icon="star" color={TagItem.docHasTag(this.carouselItems?.[this.carouselIndex], this.starField) ? 'yellow' : 'gray'} size="1x" />
- </div>
<div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
<FontAwesomeIcon icon="xmark" color="red" size="1x" />
</div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ccce2662a..fe5ced29e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -857,13 +857,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
strokeToTVals.set(inkView, [Math.floor(inkData.length / 4) + 1]);
}
}
-
for (let i = 0; i < inkData.length - 3; i += 4) {
// iterate over each segment of bezier curve
for (let j = 0; j < eraserInkData.length - 3; j += 4) {
const intersectCurve: Bezier = InkField.Segment(inkData, i); // other curve
const eraserCurve: Bezier = InkField.Segment(eraserInkData, j); // eraser curve
- this.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => {
+ InkField.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => {
// Converting the Bezier.js Split type to a t-value number.
const t = +val.toString().split('/')[0];
if (k % 2 === 0) {
@@ -1137,26 +1136,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.getClosestTs(tVals, excludeT, startIndex, mid - 1);
};
- // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
- // call in a test for linearity
- bintersects = (curve: Bezier, otherCurve: Bezier) => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((curve as any)._linear) {
- // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line
- const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] });
- if (intersections.length) {
- const intPt = otherCurve.get(intersections[0]);
- const intT = curve.project(intPt).t;
- return intT ? [intT] : [];
- }
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((otherCurve as any)._linear) {
- return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
- }
- return curve.intersects(otherCurve);
- };
-
/**
* Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
* ink strokes in the current collection.
@@ -1188,7 +1167,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) {
tVals.push(apt.t);
}
- this.bintersects(curve, otherCurve).forEach((val: string | number, ival: number) => {
+ InkField.bintersects(curve, otherCurve).forEach((val: string | number, ival: number) => {
// Converting the Bezier.js Split type to a t-value number.
const t = +val.toString().split('/')[0];
if (ival % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical).
@@ -2033,6 +2012,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
const mores = ContextMenu.Instance.findByDescription('More...');
const moreItems = mores?.subitems ?? [];
+ moreItems.push({
+ description: 'recognize all ink',
+ event: () => {
+ this.unprocessedDocs.push(...this.childDocs.filter(doc => doc.type === DocumentType.INK));
+ CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
+ },
+ icon: 'pen',
+ });
!mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
};
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index f54d8311d..b24fca8e2 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);
+ DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true));
return true;
},
emptyFunction,
diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss
index 323638bff..8a7863c14 100644
--- a/src/client/views/nodes/DiagramBox.scss
+++ b/src/client/views/nodes/DiagramBox.scss
@@ -1,5 +1,3 @@
-$searchbarHeight: 50px;
-
.DIYNodeBox {
width: 100%;
height: 100%;
@@ -23,7 +21,6 @@ $searchbarHeight: 50px;
justify-content: center;
align-items: center;
width: 100%;
- height: $searchbarHeight;
padding: 10px;
input[type='text'] {
@@ -42,7 +39,6 @@ $searchbarHeight: 50px;
justify-content: center;
align-items: center;
width: 100%;
- height: calc(100% - $searchbarHeight);
.diagramBox {
flex: 1;
display: flex;
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 36deb2d8d..d6c9bb013 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -18,7 +18,9 @@ import { InkingStroke } from '../InkingStroke';
import './DiagramBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
-
+/**
+ * this is a class for the diagram box doc type that can be found in the tools section of the side bar
+ */
@observer
export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -59,14 +61,21 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{ fireImmediately: true }
);
}
+ /**
+ * helper method for renderMermaidAsync
+ * @param str string containing the mermaid code
+ * @returns
+ */
renderMermaid = (str: string) => {
try {
return mermaid.render('graph' + Date.now(), str);
- } catch (error) {
+ } catch {
return { svg: '', bindFunctions: undefined };
}
};
-
+ /**
+ * will update the div containing the mermaid diagram to render the new mermaidCode
+ */
renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => {
try {
const { svg, bindFunctions } = await this.renderMermaid(mermaidCode);
@@ -97,7 +106,9 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
res
);
}, 'set mermaid code');
-
+ /**
+ * will generate mermaid code with GPT based on what the user requested
+ */
generateMermaidCode = action(() => {
this._generating = true;
const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index cb0c4d188..aa40b14aa 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -265,6 +265,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const toggleStatus = script?.run({ this: this.Document, self: 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 (
@@ -272,11 +273,11 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
tooltip={`Toggle ${tooltip}`}
type={Type.PRIM}
color={color}
+ background={background === SnappingManager.userBackgroundColor ? undefined : background}
multiSelect={true}
onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
isToggle={script ? true : false}
toggleStatus={toggleStatus}
- //background={SnappingManager.userBackgroundColor}
label={this.label}
items={items.map(item => ({
icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx
index 8faf8ffa5..ddabd61e1 100644
--- a/src/client/views/nodes/IconTagBox.tsx
+++ b/src/client/views/nodes/IconTagBox.tsx
@@ -67,7 +67,7 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> {
render() {
const buttons = Doc.MyFilterHotKeys
.map(key => ({ key, tag: StrCast(key.toolType) }))
- .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected.length === 1 && this.View.IsSelected))
+ .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected().length === 1 && this.View.IsSelected))
.map(({ key, tag }) => (
<Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove this card from the {tag} group</div>}>
<button
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index fd1c791d3..6289470b6 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -149,7 +149,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
componentDidMount() {
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
- this._props.setContentViewBox?.(this); // this tells the DocumentView that this Box is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+ this._props.setContentViewBox?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
// this.layoutDoc.videoWall && reaction(() => ({ width: this._props.PanelWidth(), height: this._props.PanelHeight() }),
// ({ width, height }) => {
// if (this._camera) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 123d32301..aa3d73e3e 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -103,6 +103,26 @@ export class InkField extends ObjectField {
const top = Math.min(...ys);
return { right, left, bottom, top, width: right - left, height: bottom - top };
}
+
+ // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
+ // call in a test for linearity
+ public static bintersects(curve: Bezier, otherCurve: Bezier) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((curve as any)._linear) {
+ // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line
+ const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] });
+ if (intersections.length) {
+ const intPt = otherCurve.get(intersections[0]);
+ const intT = curve.project(intPt).t;
+ return intT ? [intT] : [];
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((otherCurve as any)._linear) {
+ return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
+ }
+ return curve.intersects(otherCurve);
+ }
}
ScriptingGlobals.add('InkField', InkField);
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
index 2e1c9d16f..5a8e9bd97 100644
--- a/src/pen-gestures/GestureTypes.ts
+++ b/src/pen-gestures/GestureTypes.ts
@@ -1,7 +1,6 @@
export enum Gestures {
Line = 'line',
Stroke = 'stroke',
- Scribble = 'scribble',
Text = 'text',
Triangle = 'triangle',
Circle = 'circle',
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 31a4eb0e8..04262b61f 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -263,75 +263,7 @@ export class NDollarRecognizer {
])
)
);
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Scribble,
- useBoundedRotationInvariance,
- new Array([
- new Point(232.43359374999994, 428.3789062499997),
- new Point(232.43359374999994, 382.0359155790783),
- new Point(265.26320387389393, 340.59025405258205),
- new Point(285.92968749999994, 300.6953124999997),
- new Point(285.92968749999994, 300.6953124999997),
- new Point(290.29904726504463, 292.260623967478),
- new Point(294.85514947688137, 276.5586112000384),
- new Point(301.80468749999994, 270.1054687499997),
- new Point(301.80468749999994, 270.1054687499997),
- new Point(303.30831790356257, 268.7092405181201),
- new Point(302.3387333271824, 274.174445118092),
- new Point(302.58984374999994, 276.2109374999997),
- new Point(302.58984374999994, 276.2109374999997),
- new Point(303.5898583815426, 284.3210038050238),
- new Point(306.34100306922323, 291.42526150092345),
- new Point(308.55078124999994, 299.2578124999997),
- new Point(308.55078124999994, 299.2578124999997),
- new Point(314.02357444430737, 318.65610875195364),
- new Point(312.83820955386796, 338.9193216781303),
- new Point(317.41015624999994, 358.4374999999997),
- new Point(317.41015624999994, 358.4374999999997),
- new Point(324.6448511447705, 389.323263646351),
- new Point(341.2272901550544, 419.08257022687917),
- new Point(351.55468749999994, 449.3085937499997),
- new Point(351.55468749999994, 449.3085937499997),
- new Point(354.4485190828321, 457.7782031368645),
- new Point(359.85272292551673, 488.59621490807643),
- new Point(368.55859374999994, 492.7578124999997),
- new Point(368.55859374999994, 492.7578124999997),
- new Point(368.9336613544369, 492.9371030581646),
- new Point(375.30018285197116, 475.54269522741924),
- new Point(385.01171874999994, 460.68749999999966),
- new Point(385.01171874999994, 460.68749999999966),
- new Point(409.01141338031505, 423.9765046563952),
- new Point(465.73402430232653, 338.122252279478),
- new Point(470.49609374999994, 336.8945312499997),
- new Point(470.49609374999994, 336.8945312499997),
- new Point(472.34967396580504, 336.41665510061245),
- new Point(470.5375229924171, 340.7226912215484),
- new Point(470.56249999999994, 342.6367187499997),
- new Point(470.56249999999994, 342.6367187499997),
- new Point(470.6277554579174, 347.63734752514455),
- new Point(471.4666087447205, 352.597933700578),
- new Point(472.79687499999994, 357.4218749999997),
- new Point(472.79687499999994, 357.4218749999997),
- new Point(478.7003095537336, 378.82948542322754),
- new Point(492.1754046938961, 397.52358353298115),
- new Point(497.80468749999994, 418.6796874999997),
- new Point(497.80468749999994, 418.6796874999997),
- new Point(499.2975220287686, 424.29009358262533),
- new Point(498.6576561843205, 452.5736061983319),
- new Point(502.21874999999994, 455.3749999999997),
- new Point(502.21874999999994, 455.3749999999997),
- new Point(502.3599404176782, 455.4860697952399),
- new Point(526.9859878583989, 412.7902963819643),
- new Point(528.28515625, 410.5507812499997),
- new Point(528.28515625, 410.5507812499997),
- new Point(534.3674131478522, 400.0661462732759),
- new Point(586.26953125, 311.2520380124085),
- new Point(586.26953125, 306.8515624999997),
- ])
- )
- );
- this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(200, 0), new Point(0, 0), new Point(0, 100)])));
+ this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(0, 0), new Point(0, 100), new Point(200, 100)])));
NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
//
// PREDEFINED STROKES