From f7cdcb654e83d7fdbfd0b1cfc80c485bb9554f08 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Wed, 28 Aug 2024 22:03:12 -0400 Subject: final changes, ready for pull request --- src/client/util/Scripting.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index c63d3d7cb..cb314e3f1 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,7 +1,7 @@ // export const ts = (window as any).ts; // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' -import typescriptlib from 'type_decls.d'; +// import typescriptlib from 'type_decls.d'; import * as ts from 'typescript'; import { Doc, FieldType } from '../../fields/Doc'; import { RefField } from '../../fields/RefField'; @@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile('file.ts', funcScript); - if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); + // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); const outputText = host.readFile('file.js'); -- cgit v1.2.3-70-g09d2 From 0ac79ba6a7ab19b4aafbc11dac9bab4781d4bd40 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 29 Aug 2024 13:07:53 -0400 Subject: merge cleanup cleanup --- eslint.config.mjs | 67 +++++- src/ClientUtils.ts | 9 +- src/client/cognitive_services/CognitiveServices.ts | 15 +- src/client/util/Scripting.ts | 4 +- src/client/util/SettingsManager.tsx | 6 +- src/client/util/bezierFit.ts | 18 +- src/client/views/DocumentButtonBar.tsx | 10 +- src/client/views/GestureOverlay.tsx | 10 +- src/client/views/InkStrokeProperties.ts | 268 +++++++++++---------- src/client/views/PropertiesButtons.tsx | 4 +- .../collections/CollectionStackedTimeline.tsx | 2 +- .../views/collections/CollectionTreeView.tsx | 4 - src/client/views/collections/TreeView.tsx | 6 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 22 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +- src/client/views/pdf/Annotation.tsx | 1 + src/client/views/pdf/PDFViewer.tsx | 4 +- src/client/views/smartdraw/AnnotationPalette.tsx | 20 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 2 +- 20 files changed, 256 insertions(+), 230 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/eslint.config.mjs b/eslint.config.mjs index 12ad3300a..619966f20 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,14 +1,57 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; - +import pluginJs from '@eslint/js'; +import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; export default [ - {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]}, - {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, - {languageOptions: { globals: globals.browser }}, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - pluginReact.configs.flat.recommended, -]; \ No newline at end of file + { + languageOptions: { globals: { ...globals.browser, ...globals.node } }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + 'node/no-missing-import': 0, + 'no-console': 'off', + 'func-names': 'off', + 'no-process-exit': 'off', + 'object-shorthand': 'off', + 'class-methods-use-this': 'off', + 'single-quote': 'off', + 'max-classes-per-file': 0, + + 'react/jsx-filename-extension': [ + 2, + { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + ], + + 'import/prefer-default-export': 'off', + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + 'prefer-template': 'off', + 'no-inner-declarations': 'off', + 'no-plusplus': 'off', + 'no-multi-assign': 'off', + 'no-underscore-dangle': 'off', + 'no-nested-ternary': 'off', + 'lines-between-class-members': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'warn', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-namespace': 'off', + 'react/destructuring-assignment': 0, + 'prefer-arrow-callback': 'error', + 'no-return-assign': 'error', + 'no-await-in-loop': 'error', + 'no-loop-func': 'error', + 'no-cond-assign': 'error', + 'no-use-before-define': 'error', + 'no-explicit-any': 'error', + 'no-restricted-globals': ['error', 'event'], + }, + }, + pluginReactConfig, +]; diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index dc52218c5..55801df81 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -82,10 +82,6 @@ export function returnEmptyFilter() { return [] as string[]; } -export function returnEmptyDoclist() { - return [] as any[]; -} - export namespace ClientUtils { export const CLICK_TIME = 300; export const DRAG_THRESHOLD = 4; @@ -449,11 +445,10 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement | animateScroll(); } -export function addStyleSheet(styleType: string = 'text/css') { +export function addStyleSheet() { const style = document.createElement('style'); - style.type = styleType; const sheets = document.head.appendChild(style); - return (sheets as any).sheet; + return sheets.sheet; } export function addStyleSheetRule(sheet: CSSStyleSheet | null, selector: string, css: string | { [key: string]: string }, selectorPrefix = '.') { const propText = diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 9f7701c54..3ee61cbfb 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -46,15 +46,14 @@ export enum Confidence { export namespace CognitiveServices { const ExecuteQuery = async (service: Service, manager: APIManager, data: D): Promise => { const apiKey = process.env[service.toUpperCase()]; - if (apiKey) { - console.log(data); + if (!apiKey) { console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`); return undefined; } let results: any; try { - results = await manager.requester('has', manager.converter(data), service).then(json => JSON.parse(json)); + results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json)); } catch (e) { throw e; } @@ -138,14 +137,6 @@ export namespace CognitiveServices { points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(','), language: 'en-US', })); - console.log( - JSON.stringify({ - version: 1, - language: 'en-US', - unit: 'mm', - strokes, - }) - ); return JSON.stringify({ version: 1, language: 'en-US', @@ -345,7 +336,7 @@ export namespace CognitiveServices { 'Ocp-Apim-Subscription-Key': apiKey, }, }; - return request.post(options); + return rp.post(options); }, }; diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index cb314e3f1..c63d3d7cb 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,7 +1,7 @@ // export const ts = (window as any).ts; // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' -// import typescriptlib from 'type_decls.d'; +import typescriptlib from 'type_decls.d'; import * as ts from 'typescript'; import { Doc, FieldType } from '../../fields/Doc'; import { RefField } from '../../fields/RefField'; @@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile('file.ts', funcScript); - // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); + if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); const outputText = host.readFile('file.js'); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9e49117a7..9200d68db 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -27,7 +27,7 @@ export enum ColorScheme { } @observer -export class SettingsManager extends React.Component<{}> { +export class SettingsManager extends React.Component { // eslint-disable-next-line no-use-before-define public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); @@ -85,7 +85,7 @@ export class SettingsManager extends React.Component<{}> { if (this._playgroundMode) { DocServer.Control.makeReadOnly(); addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); + } else if (ClientUtils.CurrentUserEmail() !== 'guest') DocServer.Control.makeEditable(); }), 'set playgorund mode' ); @@ -121,7 +121,7 @@ export class SettingsManager extends React.Component<{}> { 'change color scheme' ); - constructor(props: {}) { + constructor(props: object) { super(props); makeObservable(this); SettingsManager.Instance = this; diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 693676bc3..4c7f4a0ba 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -2,7 +2,6 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable no-param-reassign */ /* eslint-disable camelcase */ -import e from 'cors'; import { Point } from '../../pen-gestures/ndollar'; export enum SVGType { @@ -628,7 +627,7 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) { export function SVGToBezier(name: SVGType, attributes: any): Point[] { switch (name) { - case 'line': + case 'line': { const x1 = parseInt(attributes.x1); const x2 = parseInt(attributes.x2); const y1 = parseInt(attributes.y1); @@ -639,8 +638,9 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { { X: x2, Y: y2 }, { X: x2, Y: y2 }, ]; + } case 'circle': - case 'ellipse': + case 'ellipse': { const c = 0.551915024494; const centerX = parseInt(attributes.cx); const centerY = parseInt(attributes.cy); @@ -664,7 +664,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { { X: centerX - c * radiusX, Y: centerY + radiusY }, { X: centerX, Y: centerY + radiusY }, ]; - case 'rect': + } + case 'rect': { const x = parseInt(attributes.x); const y = parseInt(attributes.y); const width = parseInt(attributes.width); @@ -687,14 +688,15 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { { X: x, Y: y }, { X: x, Y: y }, ]; - case 'path': + } + case 'path': { const coordList: Point[] = []; const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/); coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); const matches: RegExpMatchArray[] = Array.from( attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g) ); - let lastPt: Point; + let lastPt: Point = { X: 0, Y: 0 }; matches.forEach(match => { if (match[0].startsWith('Q')) { coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) }); @@ -725,7 +727,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { coordList.pop(); } return coordList; - case 'polygon': + } + case 'polygon': { const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g)); let list: Point[] = []; coords.forEach(coord => { @@ -737,6 +740,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { const firstPts = list.splice(0, 2); list = list.concat(firstPts); return list; + } default: return []; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 8aa4c2093..096f058ad 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { FaEdit } from 'react-icons/fa'; import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast } from '../../fields/Doc'; +import { Doc } from '../../fields/Doc'; import { Cast, DocCast } from '../../fields/Types'; import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils'; import { CalendarManager } from '../util/CalendarManager'; @@ -17,7 +17,7 @@ import { DictationManager } from '../util/DictationManager'; import { DragManager } from '../util/DragManager'; import { dropActionType } from '../util/DropActionTypes'; import { SharingManager } from '../util/SharingManager'; -import { UndoManager, undoable, undoBatch } from '../util/UndoManager'; +import { UndoManager, undoable } from '../util/UndoManager'; import './DocumentButtonBar.scss'; import { ObservableReactComponent } from './ObservableReactComponent'; import { PinProps } from './PinFuncs'; @@ -29,7 +29,6 @@ import { DocumentView } from './nodes/DocumentView'; import { OpenWhere } from './nodes/OpenWhere'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; import { AnnotationPalette } from './smartdraw/AnnotationPalette'; -import { DocData } from '../../fields/DocSymbols'; @observer export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: unknown }> { @@ -240,10 +239,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( ); } - @undoBatch - saveAnno = action(async (targetDoc: Doc) => { - await AnnotationPalette.addToPalette(targetDoc); - }); + saveAnno = undoable(async (targetDoc: Doc) => await AnnotationPalette.addToPalette(targetDoc), 'save to palette'); @computed get saveAnnoButton() { diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 63b472faf..befd19f6e 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -129,7 +129,6 @@ export class GestureOverlay extends ObservableReactComponent { - console.log('pointer up'); DocumentView.DownDocView = undefined; if (this._points.length > 1) { const B = this.svgBounds; @@ -145,9 +144,7 @@ export class GestureOverlay extends ObservableReactComponent 2 && GestureUtils.GestureRecognizer.Recognize([points]); - console.log(points); let actionPerformed = false; - console.log(result); if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) { switch (result.Name) { case Gestures.Line: @@ -156,15 +153,16 @@ export class GestureOverlay extends ObservableReactComponent { + addPoints = undoable((inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => { this.applyFunction(inkView, (view: DocumentView /* , ink: InkData */) => { const doc = view.Document; const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]]; @@ -109,7 +108,7 @@ export class InkStrokeProperties { return controls; }); - }; + }, 'add ink points'); /** * Scales a handle point of a control point that is adjacent to a newly added one. @@ -164,46 +163,48 @@ export class InkStrokeProperties { /** * Deletes the current control point of the selected ink instance. */ - @undoBatch - deletePoints = (inkView: DocumentView, preserve: boolean) => - this.applyFunction( - inkView, - (view: DocumentView, ink: InkData) => { - const doc = view.Document; - const newPoints = ink.slice(); - const brokenIndices = NumListCast(doc.brokenInkIndices); - if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) { - newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4); - } else { - const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4; - const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8)); - const samples: Point[] = []; - let startDir = { x: 0, y: 0 }; - let endDir = { x: 0, y: 0 }; - for (let i = 0; i < splicedPoints.length / 4; i++) { - const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); - if (i === 0) startDir = bez.derivative(0); - if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1); - for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) { - const pt = bez.compute(t); - samples.push(new Point(pt.x, pt.y)); - } - } - const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y }); - if (error < 100) { - newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls); + deletePoints = undoable( + (inkView: DocumentView, preserve: boolean) => + this.applyFunction( + inkView, + (view: DocumentView, ink: InkData) => { + const doc = view.Document; + const newPoints = ink.slice(); + const brokenIndices = NumListCast(doc.brokenInkIndices); + if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) { + newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4); } else { - newPoints.splice(this._currentPoint - 2, 4); + const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4; + const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8)); + const samples: Point[] = []; + let startDir = { x: 0, y: 0 }; + let endDir = { x: 0, y: 0 }; + for (let i = 0; i < splicedPoints.length / 4; i++) { + const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); + if (i === 0) startDir = bez.derivative(0); + if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1); + for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) { + const pt = bez.compute(t); + samples.push(new Point(pt.x, pt.y)); + } + } + const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y }); + if (error < 100) { + newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls); + } else { + newPoints.splice(this._currentPoint - 2, 4); + } } - } - doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control))); - runInAction(() => { - this._currentPoint = -1; - }); - return newPoints.length < 4 ? undefined : newPoints; - }, - true - ); + doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control))); + runInAction(() => { + this._currentPoint = -1; + }); + return newPoints.length < 4 ? undefined : newPoints; + }, + true + ), + 'delete ink points' + ); /** * Rotates ink stroke(s) about a point @@ -211,8 +212,7 @@ export class InkStrokeProperties { * @param angle The angle at which to rotate the ink in radians. * @param scrpt The center point of the rotation in screen coordinates */ - @undoBatch - rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => { + rotateInk = undoable((inkStrokes: DocumentView[], angle: number, scrpt: PointData) => { this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => { const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt); return !inkCenterPt @@ -224,7 +224,7 @@ export class InkStrokeProperties { return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y }; }); }); - }; + }, 'rotate ink'); /** * Rotates ink stroke(s) about a point @@ -232,8 +232,7 @@ export class InkStrokeProperties { * @param angle The angle at which to rotate the ink in radians. * @param scrpt The center point of the rotation in screen coordinates */ - @undoBatch - stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => { + stretchInk = undoable((inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => { this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData) => { const ptFromScreen = view.ComponentView?.ptFromScreen; const ptToScreen = view.ComponentView?.ptToScreen; @@ -247,77 +246,79 @@ export class InkStrokeProperties { return ptFromScreen(newscrpt); }); }); - }; + }, 'stretch ink'); /** * Handles the movement/scaling of a control point. */ - @undoBatch - moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) => - inkView && - this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { - const order = controlIndex % 4; - const closed = InkingStroke.IsClosed(ink); - const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []); - if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) { - const cptBefore = ink[controlIndex]; - const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY }; - const newink = origInk.slice(); - const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4; - const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8)); - const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt); - if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice(); - const samplesLeft: Point[] = []; - const samplesRight: Point[] = []; - let startDir = { x: 0, y: 0 }; - let endDir = { x: 0, y: 0 }; - for (let i = 0; i < nearestSeg / 4 + 1; i++) { - const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); - if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0); - if (i === nearestSeg / 4) endDir = bez.derivative(nearestT); - for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) { - const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t)); - samplesLeft.push(new Point(pt.x, pt.y)); + moveControlPtHandle = undoable( + (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) => + inkView && + this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { + const order = controlIndex % 4; + const closed = InkingStroke.IsClosed(ink); + const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []); + if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) { + const cptBefore = ink[controlIndex]; + const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY }; + const newink = origInk.slice(); + const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4; + const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8)); + const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt); + if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice(); + const samplesLeft: Point[] = []; + const samplesRight: Point[] = []; + let startDir = { x: 0, y: 0 }; + let endDir = { x: 0, y: 0 }; + for (let i = 0; i < nearestSeg / 4 + 1; i++) { + const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); + if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0); + if (i === nearestSeg / 4) endDir = bez.derivative(nearestT); + for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) { + const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t)); + samplesLeft.push(new Point(pt.x, pt.y)); + } } - } - let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y }); - for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) { - const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); - if (i === nearestSeg / 4) startDir = bez.derivative(nearestT); - if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1); - for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) { - const pt = bez.compute(Math.min(1, t)); - samplesRight.push(new Point(pt.x, pt.y)); + let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y }); + for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) { + const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); + if (i === nearestSeg / 4) startDir = bez.derivative(nearestT); + if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1); + for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) { + const pt = bez.compute(Math.min(1, t)); + samplesRight.push(new Point(pt.x, pt.y)); + } } + const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y }); + finalCtrls = finalCtrls.concat(rightCtrls); + newink.splice(this._currentPoint - 4, 8, ...finalCtrls); + return newink; } - const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y }); - finalCtrls = finalCtrls.concat(rightCtrls); - newink.splice(this._currentPoint - 4, 8, ...finalCtrls); - return newink; - } - return ink.map((pt, i) => { - const leftHandlePoint = order === 0 && i === controlIndex + 1; - const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; - if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) { - return { X: pt.X + deltaX, Y: pt.Y + deltaY }; - } - if ( - controlIndex === i || - leftHandlePoint || - rightHandlePoint || - (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || - ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) || - (order === 3 && i === controlIndex - 1) || - (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || - (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || - (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1)) - ) { - return { X: pt.X + deltaX, Y: pt.Y + deltaY }; - } - return pt; - }); - }); + return ink.map((pt, i) => { + const leftHandlePoint = order === 0 && i === controlIndex + 1; + const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; + if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) { + return { X: pt.X + deltaX, Y: pt.Y + deltaY }; + } + if ( + controlIndex === i || + leftHandlePoint || + rightHandlePoint || + (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) || + (order === 3 && i === controlIndex - 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || + (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1)) + ) { + return { X: pt.X + deltaX, Y: pt.Y + deltaY }; + } + return pt; + }); + }), + 'move ink ctrl pt' + ); public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) { let distance = Number.MAX_SAFE_INTEGER; @@ -470,26 +471,28 @@ export class InkStrokeProperties { /** * Handles the movement/scaling of a handle point. */ - @undoBatch - moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => - this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { - const doc = view.Document; - const closed = InkingStroke.IsClosed(ink); - const oldHandlePoint = ink[handleIndex]; - const oppositeHandlePoint = ink[oppositeHandleIndex]; - const controlPoint = ink[controlIndex]; - const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY }; - const inkCopy = ink.slice(); - inkCopy[handleIndex] = newHandlePoint; - const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number')); - const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; - // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). - if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { - const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint); - inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); - } - return inkCopy; - }); + moveTangentHandle = undoable( + (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => + this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { + const doc = view.Document; + const closed = InkingStroke.IsClosed(ink); + const oldHandlePoint = ink[handleIndex]; + const oppositeHandlePoint = ink[oppositeHandleIndex]; + const controlPoint = ink[controlIndex]; + const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY }; + const inkCopy = ink.slice(); + inkCopy[handleIndex] = newHandlePoint; + const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number')); + const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; + // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). + if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { + const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + } + return inkCopy; + }), + 'move ink tangent' + ); /** * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and @@ -497,8 +500,7 @@ export class InkStrokeProperties { * @param inkDocs * @param tolerance Determines how strong the smooth effect will be */ - @undoBatch - smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => { + smoothInkStrokes = undoable((inkDocs: Doc[], tolerance: number = 5) => { inkDocs.forEach(inkDoc => { const inkView = DocumentView.getDocumentView(inkDoc); const inkStroke = inkView?.ComponentView as InkingStroke; @@ -530,5 +532,5 @@ export class InkStrokeProperties { } } }); - }; + }, 'smooth ink stroke'); } diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index b94041642..f346d4ba8 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -13,7 +13,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit import { RxWidth } from 'react-icons/rx'; import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; import { TfiBarChart } from 'react-icons/tfi'; -import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { Doc, Opt } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, ScriptCast, StrCast } from '../../fields/Types'; @@ -28,8 +28,6 @@ import { Colors } from './global/globalEnums'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhere } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { MarqueeOptionsMenu } from './collections/collectionFreeForm'; -import { InkStrokeProperties } from './InkStrokeProperties'; @observer export class PropertiesButtons extends React.Component { diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 98ddc61f9..486c826b6 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -847,7 +847,7 @@ class StackedTimelineAnchor extends ObservableReactComponent>() { public static AddTreeFunc = 'addTreeFolder(this.embedContainer)'; private _treedropDisposer?: DragManager.DragDropDisposer; - private _mainEle?: HTMLDivElement; private _titleRef?: HTMLDivElement | HTMLInputElement | null; private _disposers: { [name: string]: IReactionDisposer } = {}; private _isDisposing = false; // notes that instance is in process of being disposed @@ -81,8 +80,6 @@ export class CollectionTreeView extends CollectionSubView this._mainEle; - // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; whenChildContentsActiveChanged = action((isActive: boolean) => { @@ -132,7 +129,6 @@ export class CollectionTreeView extends CollectionSubView { this._treedropDisposer?.(); - this._mainEle = ele; if (ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this)); }; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 847ff5491..015f77ffd 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -472,7 +472,7 @@ export class TreeView extends ObservableReactComponent { const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref); return new Transform(-translateX, -translateY, 1).scale(1 / scale); }; - docTransform = () => this.refTransform(this._dref?.ContentRef?.current); + docTransform = () => this.refTransform(this._dref?.ContentDiv); getTransform = () => this.refTransform(this._tref.current); embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1); embeddedPanelHeight = () => { @@ -754,7 +754,7 @@ export class TreeView extends ObservableReactComponent { } get onCheckedClick() { - return this.Document.type === DocumentType.COL ? undefined : this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick); + return this.Document.type === DocumentType.COL ? undefined : (this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick)); } @action @@ -779,7 +779,7 @@ export class TreeView extends ObservableReactComponent { TraceMobx(); const iconType = (this.treeView._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':treeOpen' : !this.childDocs.length ? ':empty' : '')) as string) || 'question'; const color = SettingsManager.userColor; - const checked = this.onCheckedClick ? this.Document.treeView_Checked ?? 'unchecked' : undefined; + const checked = this.onCheckedClick ? (this.Document.treeView_Checked ?? 'unchecked') : undefined; return (
- ); + )); }; render() { @@ -994,7 +994,7 @@ export class DocumentViewInternal extends DocComponent() { finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. ) => Promise; public static linkCommonAncestor: (link: Doc) => DocumentView | undefined; - // pin func + /** + * Pins a Doc to the current presentation trail. (see TabDocView for implementation) + */ public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void; - // gesture + /** + * 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 - public ContentRef = React.createRef(); private _htmlOverlayEffect: Opt; private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewTimer: NodeJS.Timeout | undefined; @@ -1468,11 +1471,10 @@ export class DocumentView extends DocComponent() { {!this.Document || !this._props.PanelWidth() ? null : (
(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); - static _highlightStyleSheet: any = addStyleSheet(); - static _bulletStyleSheet: any = addStyleSheet(); - static _userStyleSheet: any = addStyleSheet(); + static _highlightStyleSheet = addStyleSheet(); + static _bulletStyleSheet = addStyleSheet(); + static _userStyleSheet = addStyleSheet(); static _hadSelection: boolean = false; private _oldWheel: HTMLDivElement | null = null; private _selectionHTML: string | undefined; @@ -361,7 +361,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ) : ( -
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}> +
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}> { outline = () => (this.linkHighlighted ? 'solid 1px lightBlue' : undefined); background = () => (this._props.annoDoc[Highlight] ? 'orange' : StrCast(this._props.annoDoc.backgroundColor)); render() { + const forceRenderHack = [this.background(), this.outline(), this.opacity()]; // forces a re-render when these change -- because RegionAnnotation doesn't do this internally.. return (
{StrListCast(this._props.annoDoc.text_inlineAnnotations) diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 5984905d0..20719442a 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -52,7 +52,7 @@ interface IViewerProps extends FieldViewProps { */ @observer export class PDFViewer extends ObservableReactComponent { - static _annotationStyle: any = addStyleSheet(); + static _annotationStyle = addStyleSheet(); constructor(props: IViewerProps) { super(props); @@ -522,7 +522,7 @@ export class PDFViewer extends ObservableReactComponent { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none'; const isInk = doc.layout_isSvg && !props?.LayoutTemplateString; - return isInk ? 'visiblePainted' : 'all'; + if (isInk) return 'visiblePainted'; } return this._props.styleProvider?.(doc, props, property); }; diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx index c296138a8..22b8b7b67 100644 --- a/src/client/views/smartdraw/AnnotationPalette.tsx +++ b/src/client/views/smartdraw/AnnotationPalette.tsx @@ -6,25 +6,25 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; -import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; +import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; +import { Copy } from '../../../fields/FieldSymbols'; import { ImageCast } from '../../../fields/Types'; -import { emptyFunction } from '../../../Utils'; +import { ImageField } from '../../../fields/URLField'; +import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { makeUserTemplateImage } from '../../util/DropConverter'; import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { FieldView } from '../nodes/FieldView'; -import { ObservableReactComponent } from '../ObservableReactComponent'; -import { DefaultStyleProvider } from '../StyleProvider'; import './AnnotationPalette.scss'; import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { ImageField } from '../../../fields/URLField'; -import { Copy } from '../../../fields/FieldSymbols'; interface AnnotationPaletteProps { Document: Doc; @@ -204,7 +204,7 @@ export class AnnotationPalette extends ObservableReactComponent { const svgStrokes: INode[] = svgObject.children; const strokeData: [InkData, string, string][] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any - svgStrokes.forEach((child: any) => { + svgStrokes.forEach((child) => { const convertedBezier: InkData = SVGToBezier(child.name, child.attributes); strokeData.push([ convertedBezier.map(point => { -- cgit v1.2.3-70-g09d2 From 692076b1356309111c4f2cb69cbdbf4be1a825bd Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Sun, 22 Sep 2024 15:40:19 -0400 Subject: small bug fixes for smart draw --- src/client/util/Scripting.ts | 4 +- src/client/util/bezierFit.ts | 4 +- src/client/views/DocumentButtonBar.tsx | 15 ---- src/client/views/InkStrokeProperties.ts | 35 ++++------ src/client/views/PropertiesView.tsx | 6 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 16 +++-- src/client/views/smartdraw/AnnotationPalette.scss | 46 +++++++++++++ src/client/views/smartdraw/AnnotationPalette.tsx | 56 ++++++++++----- src/client/views/smartdraw/SmartDrawHandler.scss | 41 +++++++++++ src/client/views/smartdraw/SmartDrawHandler.tsx | 80 ++++++++++++++++------ 10 files changed, 213 insertions(+), 90 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index c63d3d7cb..cb314e3f1 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,7 +1,7 @@ // export const ts = (window as any).ts; // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' -import typescriptlib from 'type_decls.d'; +// import typescriptlib from 'type_decls.d'; import * as ts from 'typescript'; import { Doc, FieldType } from '../../fields/Doc'; import { RefField } from '../../fields/RefField'; @@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile('file.ts', funcScript); - if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); + // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); const outputText = host.readFile('file.js'); diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 4c7f4a0ba..4aef28e6b 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -696,7 +696,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { const matches: RegExpMatchArray[] = Array.from( attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g) ); - let lastPt: Point = { X: 0, Y: 0 }; + let lastPt: Point = startPt; matches.forEach(match => { if (match[0].startsWith('Q')) { coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) }); @@ -711,7 +711,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) }); lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) }; } else { - coordList.push(lastPt || { X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + coordList.push(lastPt); coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 096f058ad..04c1d359c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -239,20 +239,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( ); } - saveAnno = undoable(async (targetDoc: Doc) => await AnnotationPalette.addToPalette(targetDoc), 'save to palette'); - - @computed - get saveAnnoButton() { - const targetDoc = this.view0?.Document; - return !targetDoc ? null : ( - {targetDoc.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette'}
}> -
this.saveAnno(targetDoc)}> - -
- - ); - } - @computed get shareButton() { const targetDoc = this.view0?.Document; @@ -473,7 +459,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{this.templateButton}
{!DocumentView.Selected().some(v => v.allLinks.length) ? null :
{this.followLinkButton}
}
{this.pinButton}
-
{this.saveAnnoButton}
{this.recordButton}
{this.calendarButton}
{this.keywordButton}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 13807c25f..5cacde0d4 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -499,31 +499,20 @@ export class InkStrokeProperties { const inkView = DocumentView.getDocumentView(inkDoc); const inkStroke = inkView?.ComponentView as InkingStroke; const { inkData } = inkStroke.inkScaledData(); - - const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]); - console.log(result); - if (result && (result.Name === 'line' ? result.Score > 0.92 : result.Score > 0.85)) { - switch (result.Name) { - case Gestures.Line: - case Gestures.Triangle: - case Gestures.Rectangle: - case Gestures.Circle: - GestureOverlay.makeBezierPolygon(inkData, result.Name, true); - break; - default: - } - } else { - const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore - if (polylinePoints.length > 2) { - const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore - for (var i = 4; i < inkData.length - 3; i += 4) { - const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y); - if (!contains) { - this._currentPoint = i; - inkView && this.deletePoints(inkView, false); - } + const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore + if (polylinePoints.length > 2) { + const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore + for (var i = 4; i < inkData.length - 3; i += 4) { + const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y); + if (!contains) { + this._currentPoint = i; + inkView && this.deletePoints(inkView, false); } } + // close the curve if the first and last points are really close (based on tolerance) + if (!InkingStroke.IsClosed(inkData) && Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) <= tolerance * 2) { + inkData[inkData.length - 1] = inkData[0]; + } } }); }, 'smooth ink stroke'); diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 9299705ee..39ed16f5d 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -991,12 +991,12 @@ export class PropertiesView extends ObservableReactComponent { !isNaN(val) && (this.smoothAmt = val); }, - 20, + 10, 1 )}
@@ -1021,7 +1021,7 @@ export class PropertiesView extends ObservableReactComponent { + removeDrawing = (useLastContainer: boolean, doc?: Doc) => { this._batch = UndoManager.StartBatch('regenerateDrawing'); - if (doc) { + if (useLastContainer && this._drawingContainer) { + this._props.removeDocument?.(this._drawingContainer); + } else if (doc) { const docData = doc[DocData]; const children = DocListCast(docData.data); this._props.removeDocument?.(doc); this._props.removeDocument?.(children); - } else { - if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer); } this._drawing = []; }; @@ -1294,6 +1296,7 @@ export class CollectionFreeFormView extends CollectionSubView { 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; @@ -2010,6 +2013,11 @@ export class CollectionFreeFormView extends CollectionSubView await AnnotationPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down', + }); this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss index 9f875f61a..4f11e8afc 100644 --- a/src/client/views/smartdraw/AnnotationPalette.scss +++ b/src/client/views/smartdraw/AnnotationPalette.scss @@ -8,3 +8,49 @@ border-radius: 5px; margin: auto; } + +.palette-create { + display: flex; + flex-direction: row; + width: 170px; + + .palette-create-input { + color: black; + width: 170px; + } +} + +.palette-create-options { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 170px; + margin-top: 5px; + + .palette-color { + display: flex; + flex-direction: column; + align-items: center; + width: 40px; + } + + .palette-detail, + .palette-size { + display: flex; + flex-direction: column; + align-items: center; + width: 60px; + } +} + +.palette-buttons { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.palette-save-reset { + display: flex; + flex-direction: row; +} diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx index a2d6cc88d..0c8dbf12d 100644 --- a/src/client/views/smartdraw/AnnotationPalette.tsx +++ b/src/client/views/smartdraw/AnnotationPalette.tsx @@ -23,11 +23,22 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { FieldView } from '../nodes/FieldView'; import './AnnotationPalette.scss'; import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; +import { ImageField } from '../../../fields/URLField'; +import { Copy } from '../../../fields/FieldSymbols'; interface AnnotationPaletteProps { Document: Doc; } +/** + * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette + * is to offer an easy way for users to save then drag and drop repeated annotations onto a document. + * These annotations can be of any annotation type and operate similarly to user templates. + * + * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can + * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing + * to choose from. These drawings can then be saved to the palette as annotations. + */ @observer export class AnnotationPalette extends ObservableReactComponent { @observable private _paletteMode: 'create' | 'view' = 'view'; @@ -107,20 +118,25 @@ export class AnnotationPalette extends ObservableReactComponent { if (!doc.savedAsAnno) { - Doc.MakeClone(doc).then(cloneMap => - DocumentView.getDocumentView(doc) - ?.ComponentView?.updateIcon?.(true) - .then(() => { - const { clone } = cloneMap; - clone.title = doc.title; - const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; - Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image)); - doc.savedAsAnno = true; - }) - ); + const docView = DocumentView.getDocumentView(doc); + await docView?.ComponentView?.updateIcon?.(true); + const { clone } = await Doc.MakeClone(doc); + clone.title = doc.title; + const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; + Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image)); + doc.savedAsAnno = true; } }; + public static getIcon(group: Doc) { + const docView = DocumentView.getDocumentView(group); + if (docView) { + docView.ComponentView?.updateIcon?.(true); + return new Promise(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + /** * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from * the annotation palette. @@ -172,6 +188,8 @@ export class AnnotationPalette extends ObservableReactComponent -
+
{ this.setUserInput(e.target.value); @@ -228,8 +246,8 @@ export class AnnotationPalette extends ObservableReactComponent
-
-
+
+
Color this.setColor(!this._opts.autoColor)} />
-
+
Detail
-
+
Size -
+
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss index 6d402a80f..0e8bd3349 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.scss +++ b/src/client/views/smartdraw/SmartDrawHandler.scss @@ -1,3 +1,44 @@ .smart-draw-handler { position: absolute; } + +.smartdraw-input { + color: black; +} + +.smartdraw-options { + display: flex; + flex-direction: row; + justify-content: space-around; + + .auto-color { + display: flex; + flex-direction: column; + justify-content: center; + width: 30%; + } + + .complexity { + display: flex; + flex-direction: column; + justify-content: center; + width: 31%; + } + + .size { + display: flex; + flex-direction: column; + justify-content: center; + width: 39%; + + .size-slider { + width: 80%; + } + } +} + +.regenerate-box, +.edit-box { + display: flex; + flex-direction: row; +} diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 8271c0959..e362c0c89 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -12,7 +12,6 @@ import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { InkData, InkField, InkTool } from '../../../fields/InkField'; import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { SettingsManager } from '../../util/SettingsManager'; @@ -34,6 +33,21 @@ export interface DrawingOptions { y: number; } +/** + * The SmartDrawHandler allows users to generate drawings with GPT from text input. Users are able to enter + * the item to draw, how complex they want the drawing to be, how large the drawing should be, and whether + * it will be colored. If the drawing is colored, GPT will automatically define the stroke and fill of each + * stroke. Drawings are retrieved from GPT as SVG code then converted into Dash-supported Beziers. + * + * The handler is selected from the ink tools menu. To generate a drawing, users can click anywhere on the freeform + * canvas and a popup will appear that prompts them to create a drawing. Once the drawing is created, users have + * the option to regenerate or edit the drawing. + * + * When each drawing is created, it is added to Dash as a group of ink strokes. The group is tagged with metadata + * for user input, the drawing's SVG code, and its settings (size, complexity). In the context menu -> 'Options', + * users can then show the drawing editor and regenerate/edit them at any point in the future. + */ + @observer export class SmartDrawHandler extends ObservableReactComponent { static Instance: SmartDrawHandler; @@ -65,8 +79,19 @@ export class SmartDrawHandler extends ObservableReactComponent { SmartDrawHandler.Instance = this; } + /** + * AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e. + CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added + or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.) + */ public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction; - public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction; + public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction; + /** + * This creates the ink document that represents a drawing, so it goes through the strokes that make up the drawing, + * creates ink documents for each stroke, then adds the strokes to a collection. This can also be redefined by other + * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of + * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions. + */ public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => { const drawing: Doc[] = []; strokeList.forEach((stroke: [InkData, string, string]) => { @@ -101,6 +126,11 @@ export class SmartDrawHandler extends ObservableReactComponent { this._display = true; }; + /** + * This is called in two places: 1. In this class, where the regenerate popup shows as soon as a + * drawing is created to replace the original smart draw popup. 2. From the context menu to make + * the regenerate popup show by user command. + */ @action displayRegenerate = (x: number, y: number) => { this._selectedDoc = DocumentView.SelectedDocs()?.lastElement(); @@ -113,6 +143,9 @@ export class SmartDrawHandler extends ObservableReactComponent { this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY }; }; + /** + * Hides the smart draw handler and resets its fields to their default. + */ @action hideSmartDrawHandler = () => { this.ShowRegenerate = false; @@ -126,6 +159,9 @@ export class SmartDrawHandler extends ObservableReactComponent { Doc.ActiveTool = InkTool.None; }; + /** + * Hides the popup that allows users to regenerate a drawing and resets its corresponding fields. + */ @action hideRegenerate = () => { if (!this._isLoading) { @@ -136,12 +172,20 @@ export class SmartDrawHandler extends ObservableReactComponent { } }; + /** + * This allows users to press the return/enter key to send input. + */ handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { this.handleSendClick(); } }; + /** + * This is called when a user hits "send" on the draw with GPT popup. It calls the drawWithGPT or regenerate + * functions depending on what mode is currently displayed, then sets various observable fields that facilitate + * what the user sees. + */ @action handleSendClick = async () => { this._isLoading = true; @@ -171,7 +215,7 @@ export class SmartDrawHandler extends ObservableReactComponent { }; /** - * Calls GPT API to create a drawing based on user input + * Calls GPT API to create a drawing based on user input. */ @action drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { @@ -182,6 +226,7 @@ export class SmartDrawHandler extends ObservableReactComponent { console.error('GPT call failed'); return; } + console.log(res); const strokeData = await this.parseSvg(res, startPt, false, autoColor); const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes); drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); @@ -213,7 +258,7 @@ export class SmartDrawHandler extends ObservableReactComponent { 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(this._selectedDoc); + this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc); const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes); drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); return strokeData; @@ -223,7 +268,7 @@ export class SmartDrawHandler extends ObservableReactComponent { }; /** - * Parses the svg code that GPT returns into Bezier curves. + * Parses the svg code that GPT returns into Bezier curves, with coordinates and colors. */ @action parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => { @@ -307,26 +352,18 @@ export class SmartDrawHandler extends ObservableReactComponent { }} icon={} color={SettingsManager.userColor} - style={{ width: '19px' }} /> this._canInteract && (this._userInput = e.target.value))} placeholder="Enter item to draw" onKeyDown={this.handleKeyPress} /> - } - color={SettingsManager.userColor} - style={{ width: '14px' }} - onClick={action(() => (this._showOptions = !this._showOptions))} - /> + } color={SettingsManager.userColor} onClick={action(() => (this._showOptions = !this._showOptions))} />