aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json6
-rw-r--r--package.json1
-rw-r--r--src/client/apis/gpt/GPT.ts45
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts18
-rw-r--r--src/client/views/GestureOverlay.tsx150
-rw-r--r--src/client/views/InkStrokeProperties.ts43
-rw-r--r--src/client/views/InkTranscription.tsx4
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/PropertiesButtons.tsx4
-rw-r--r--src/client/views/PropertiesView.scss6
-rw-r--r--src/client/views/PropertiesView.tsx180
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx106
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx80
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx8
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx38
-rw-r--r--src/client/views/pdf/PDFViewer.tsx10
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.tsx44
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx155
19 files changed, 669 insertions, 235 deletions
diff --git a/package-lock.json b/package-lock.json
index 70f53156c..7f6237ef5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -214,6 +214,7 @@
"sass-loader": "^14.2.0",
"serializr": "^3.0.2",
"shelljs": "^0.8.5",
+ "simplify-js": "^1.2.4",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"standard-http-error": "^2.0.1",
@@ -38407,6 +38408,11 @@
"node": ">=10"
}
},
+ "node_modules/simplify-js": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/simplify-js/-/simplify-js-1.2.4.tgz",
+ "integrity": "sha512-vITfSlwt7h/oyrU42R83mtzFpwYk3+mkH9bOHqq/Qw6n8rtR7aE3NZQ5fbcyCUVVmuMJR6ynsAhOfK2qoah8Jg=="
+ },
"node_modules/skmeans": {
"version": "0.9.7",
"resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz",
diff --git a/package.json b/package.json
index 8929bddf0..16b2841be 100644
--- a/package.json
+++ b/package.json
@@ -299,6 +299,7 @@
"sass-loader": "^14.2.0",
"serializr": "^3.0.2",
"shelljs": "^0.8.5",
+ "simplify-js": "^1.2.4",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"standard-http-error": "^2.0.1",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index a780596fa..ee8d5e9b2 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -13,6 +13,7 @@ enum GPTCallType {
MERMAID = 'mermaid',
DATA = 'data',
DRAW = 'draw',
+ COLOR = 'color',
}
type GPTCallOpts = {
@@ -57,9 +58,15 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
draw: {
model: 'gpt-4o',
maxTokens: 1024,
- temp: 0.5,
+ temp: 0.8,
prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, polygon, and path with M, Q, C, and L so only use those.',
},
+ color: {
+ model: 'gpt-4o',
+ maxTokens: 1024,
+ temp: 0.5,
+ prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
+ },
};
let lastCall = '';
@@ -187,4 +194,38 @@ const gptHandwriting = async (src: string): Promise<string> => {
}
};
-export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting };
+const gptDrawingColor = async (image: string, coords: string[]): Promise<string> => {
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o',
+ temperature: 0,
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: `Identify what the drawing in the image represents in 1-5 words. Then, given a list of a list of coordinates, where each list is the coordinates for one stroke of the drawing, determine which part of the drawing it is. Return just what the item it is, followed by ~~~ then only your descriptions in a list like [description, description, ...]. Here are the coordinates: ${coords}`,
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: `${image}`,
+ detail: 'low',
+ },
+ },
+ ],
+ },
+ ],
+ });
+ if (response.choices[0].message.content) {
+ return response.choices[0].message.content;
+ }
+ return 'Missing labels';
+ } catch (err) {
+ console.log(err);
+ return 'Error connecting with API';
+ }
+};
+
+export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDrawingColor };
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9f46b8685..9f7701c54 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -47,14 +47,14 @@ export namespace CognitiveServices {
const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
const apiKey = process.env[service.toUpperCase()];
if (apiKey) {
- console.log(data)
+ console.log(data);
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('has', manager.converter(data), service).then(json => JSON.parse(json));
} catch (e) {
throw e;
}
@@ -138,12 +138,14 @@ 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,
- }))
+ console.log(
+ JSON.stringify({
+ version: 1,
+ language: 'en-US',
+ unit: 'mm',
+ strokes,
+ })
+ );
return JSON.stringify({
version: 1,
language: 'en-US',
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 649208989..7eac583dd 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -141,7 +141,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
// if any of the shape is activated in the CollectionFreeFormViewChrome
if (this.InkShape) {
- this.makeBezierPolygon(this.InkShape, false);
+ GestureOverlay.makeBezierPolygon(this._points, this.InkShape, false);
this.dispatchGesture(this.InkShape);
this.primCreated();
}
@@ -158,7 +158,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
case Gestures.Triangle:
case Gestures.Rectangle:
case Gestures.Circle:
- this.makeBezierPolygon(result.Name, true);
+ GestureOverlay.makeBezierPolygon(this._points, result.Name, true);
actionPerformed = this.dispatchGesture(result.Name);
console.log(result.Name);
console.log();
@@ -202,17 +202,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this._points.length = 0;
};
- makeBezierPolygon = (shape: string, gesture: boolean) => {
- const xs = this._points.map(p => p.X);
- const ys = this._points.map(p => p.Y);
+ 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);
let right = Math.max(...xs);
let left = Math.min(...xs);
let bottom = Math.max(...ys);
let top = Math.min(...ys);
- const firstx = this._points[0].X;
- const firsty = this._points[0].Y;
- let lastx = this._points[this._points.length - 2].X;
- let lasty = this._points[this._points.length - 2].Y;
+ const firstx = points[0].X;
+ const firsty = points[0].Y;
+ let lastx = points[points.length - 2].X;
+ let lasty = points[points.length - 2].Y;
let fourth = (lastx - firstx) / 4;
if (isNaN(fourth) || fourth === 0) {
fourth = 0.01;
@@ -223,15 +223,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
// const b = firsty - m * firstx;
if (shape === 'noRec') {
- return false;
+ return undefined;
}
if (!gesture) {
// if shape options is activated in inkOptionMenu
// take second to last point because _point[length-1] is _points[0]
- right = this._points[this._points.length - 2].X;
- left = this._points[0].X;
- bottom = this._points[this._points.length - 2].Y;
- top = this._points[0].Y;
+ right = points[points.length - 2].X;
+ left = points[0].X;
+ bottom = points[points.length - 2].Y;
+ top = points[0].Y;
if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
if (left > right) {
const temp = right;
@@ -245,47 +245,47 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
}
}
- this._points.length = 0;
+ points.length = 0;
switch (shape) {
case Gestures.Rectangle:
- this._points.push({ X: left, Y: top }); // curr pt
- this._points.push({ X: left, Y: top }); // curr first ctrl pt
- this._points.push({ X: right, Y: top }); // next ctrl pt
- this._points.push({ X: right, Y: top }); // next pt
-
- this._points.push({ X: right, Y: top }); // next pt
- this._points.push({ X: right, Y: top }); // next first ctrl pt
- this._points.push({ X: right, Y: bottom }); // next next ctrl pt
- this._points.push({ X: right, Y: bottom }); // next next pt
-
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
-
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: top });
- this._points.push({ X: left, Y: top });
+ points.push({ X: left, Y: top }); // curr pt
+ points.push({ X: left, Y: top }); // curr first ctrl pt
+ points.push({ X: right, Y: top }); // next ctrl pt
+ points.push({ X: right, Y: top }); // next pt
+
+ points.push({ X: right, Y: top }); // next pt
+ points.push({ X: right, Y: top }); // next first ctrl pt
+ points.push({ X: right, Y: bottom }); // next next ctrl pt
+ points.push({ X: right, Y: bottom }); // next next pt
+
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: top });
+ points.push({ X: left, Y: top });
break;
case Gestures.Triangle:
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
- this._points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
break;
case Gestures.Circle:
@@ -299,25 +299,25 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
// Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
- this._points.push({ X: centerX, Y: centerY + radius }); // curr pt
- this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
- this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
- this._points.push({ X: centerX + radius, Y: centerY }); // next pt
-
- this._points.push({ X: centerX + radius, Y: centerY }); // next pt
- this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
- this._points.push({ X: centerX + c * radius, Y: centerY - radius });
- this._points.push({ X: centerX, Y: centerY - radius });
-
- this._points.push({ X: centerX, Y: centerY - radius });
- this._points.push({ X: centerX - c * radius, Y: centerY - radius });
- this._points.push({ X: centerX - radius, Y: centerY - c * radius });
- this._points.push({ X: centerX - radius, Y: centerY });
-
- this._points.push({ X: centerX - radius, Y: centerY });
- this._points.push({ X: centerX - radius, Y: centerY + c * radius });
- this._points.push({ X: centerX - c * radius, Y: centerY + radius });
- this._points.push({ X: centerX, Y: centerY + radius });
+ points.push({ X: centerX, Y: centerY + radius }); // curr pt
+ points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+ points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+ points.push({ X: centerX + radius, Y: centerY }); // next pt
+
+ points.push({ X: centerX + radius, Y: centerY }); // next pt
+ points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
+ points.push({ X: centerX + c * radius, Y: centerY - radius });
+ points.push({ X: centerX, Y: centerY - radius });
+
+ points.push({ X: centerX, Y: centerY - radius });
+ points.push({ X: centerX - c * radius, Y: centerY - radius });
+ points.push({ X: centerX - radius, Y: centerY - c * radius });
+ points.push({ X: centerX - radius, Y: centerY });
+
+ points.push({ X: centerX - radius, Y: centerY });
+ points.push({ X: centerX - radius, Y: centerY + c * radius });
+ points.push({ X: centerX - c * radius, Y: centerY + radius });
+ points.push({ X: centerX, Y: centerY + radius });
}
break;
@@ -328,11 +328,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) {
lasty = firsty;
}
- this._points.push({ X: firstx, Y: firsty });
- this._points.push({ X: firstx, Y: firsty });
+ points.push({ X: firstx, Y: firsty });
+ points.push({ X: firstx, Y: firsty });
- this._points.push({ X: lastx, Y: lasty });
- this._points.push({ X: lastx, Y: lasty });
+ points.push({ X: lastx, Y: lasty });
+ points.push({ X: lastx, Y: lasty });
break;
case Gestures.Arrow:
{
@@ -347,16 +347,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
- this._points.push({ X: x1, Y: y1 });
- this._points.push({ X: x2, Y: y2 });
- this._points.push({ X: x3, Y: y3 });
- this._points.push({ X: x4, Y: y4 });
- this._points.push({ X: x2, Y: y2 });
+ points.push({ X: x1, Y: y1 });
+ points.push({ X: x2, Y: y2 });
+ points.push({ X: x3, Y: y3 });
+ points.push({ X: x4, Y: y4 });
+ points.push({ X: x2, Y: y2 });
}
break;
default:
}
- return false;
+ return points;
};
dispatchGesture = (gesture: Gestures, stroke?: InkData, text?: any) => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 3920ecc2a..35d628a4e 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -5,8 +5,8 @@ import { Doc, NumListCast, Opt } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
-import { Cast, NumCast } from '../../fields/Types';
-import { PointData } from '../../pen-gestures/GestureTypes';
+import { Cast, NumCast, toList } from '../../fields/Types';
+import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
import { Point } from '../../pen-gestures/ndollar';
import { DocumentType } from '../documents/DocumentTypes';
import { undoBatch } from '../util/UndoManager';
@@ -14,6 +14,9 @@ import { FitOneCurve } from '../util/bezierFit';
import { InkingStroke } from './InkingStroke';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { DocumentView } from './nodes/DocumentView';
+import simplify from 'simplify-js';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+import { GestureOverlay } from './GestureOverlay';
export class InkStrokeProperties {
// eslint-disable-next-line no-use-before-define
@@ -487,4 +490,40 @@ export class InkStrokeProperties {
}
return inkCopy;
});
+
+ @undoBatch
+ smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => {
+ inkDocs.forEach(inkDoc => {
+ 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);
+ let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
+ 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);
+ }
+ }
+ }
+ }
+ });
+ };
}
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 3f90df7d1..1277a6fe7 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -6,7 +6,7 @@ import { InkData, InkField, InkTool } from '../../fields/InkField';
import { Cast, DateCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
import { aggregateBounds } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm';
import { InkingStroke } from './InkingStroke';
import './InkTranscription.scss';
import { Docs } from '../documents/Documents';
@@ -387,7 +387,7 @@ export class InkTranscription extends React.Component {
);
docView.props.removeDocument?.(selected);
// Gets a collection based on the selected nodes using a marquee view ref
- const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+ const newCollection = MarqueeView.getCollection(selected, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
if (newCollection) {
newCollection.width = NumCast(newCollection._width);
newCollection.height = NumCast(newCollection._height);
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ce1c07f2f..48b26d903 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -104,10 +104,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
* analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
* and the recognized words to the 'handwriting'
*/
- analyzeStrokes=()=> {
+ analyzeStrokes = () => {
const data: InkData = this.inkScaledData().inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
- }
+ };
/**
* Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index edf6df2b9..de1800700 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -15,7 +15,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, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { ScriptField } from '../../fields/ScriptField';
import { BoolCast, ScriptCast } from '../../fields/Types';
@@ -30,6 +30,8 @@ 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/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 840df41e7..aa825a6e9 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -638,3 +638,9 @@
padding-left: 8px;
background-color: rgb(51, 51, 51);
}
+
+.smooth,
+.color,
+.smooth-slider {
+ margin-top: 3px;
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 024db82a4..ac2625f32 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -5,7 +5,7 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core';
import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, Tooltip } from '@mui/material';
-import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
+import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components';
import { concat } from 'lodash';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
@@ -14,13 +14,13 @@ import { ColorResult, SketchPicker } from 'react-color';
import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
import { List } from '../../fields/List';
import { ComputedField } from '../../fields/ScriptField';
-import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { GroupManager } from '../util/GroupManager';
@@ -43,6 +43,10 @@ import { DocumentView } from './nodes/DocumentView';
import { StyleProviderFuncType } from './nodes/FieldView';
import { OpenWhere } from './nodes/OpenWhere';
import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+import { InkingStroke } from './InkingStroke';
+import { SettingsManager } from '../util/SettingsManager';
+import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
const _global = (window /* browser */ || global) /* node */ as any;
@@ -117,6 +121,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
@observable openAddSlide: boolean = false;
@observable openSlideOptions: boolean = false;
+ // For ink groups
+ @observable containsInkDoc: boolean = false;
+ @observable inkDoc: Doc | undefined = undefined;
+
@observable _controlButton: boolean = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -794,7 +802,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
};
getField(key: string) {
- return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
+ return this.containsInkDoc ? Field.toString(this.inkDoc?.[DocData][key] as FieldType) : Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
}
@computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
@@ -805,8 +813,17 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
set shapeWid(value) { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
@computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
set shapeHgt(value) { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
- @computed get strokeThk(){ return NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
- set strokeThk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); } // prettier-ignore
+ @computed get strokeThk(){ return this.containsInkDoc ? NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
+ set strokeThk(value) {
+ if (this.containsInkDoc) {
+ const childDocs = DocListCast(this.selectedDoc[DocData].data);
+ childDocs.forEach(doc => {
+ doc[DocData].stroke_width = Math.round(value * 100) / 100;
+ })
+ } else {
+ this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100);
+ }
+ } // prettier-ignore
@computed get hgtInput() {
return this.inputBoxDuo(
@@ -843,10 +860,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
private _lastDash: any = '2';
- @computed get colorFil() { return StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
- set colorFil(value) { this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); } // prettier-ignore
- @computed get colorStk() { return StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
- set colorStk(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
+ @computed get colorFil() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].fillColor) : StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
+ set colorFil(value) {
+ if (this.containsInkDoc) {
+ const childDocs = DocListCast(this.selectedDoc[DocData].data);
+ childDocs.forEach(doc => {
+ const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke;
+ const { inkData } = inkStroke.inkScaledData();
+ if (InkingStroke.IsClosed(inkData)) {
+ doc[DocData].fillColor = value || undefined;
+ }
+ });
+ } else {
+ this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined);
+ }
+ }
+ @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
+ set colorStk(value) {
+ if (this.containsInkDoc) {
+ const childDocs = DocListCast(this.selectedDoc[DocData].data);
+ childDocs.forEach(doc => {
+ doc[DocData].color = value || undefined;
+ });
+ } else {
+ this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined);
+ }
+ }
colorButton(value: string, type: string, setter: () => void) {
return (
@@ -917,10 +956,69 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
);
}
- @computed get dashdStk() { return this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
+ @computed get smoothAndColor() {
+ const targetDoc = this.selectedLayoutDoc;
+ return (
+ <div>
+ <div className="smooth">
+ <Toggle
+ text={'Smooth Ink Strokes'}
+ color={SettingsManager.userColor}
+ icon={<FontAwesomeIcon icon="bezier-curve" />}
+ iconPlacement="left"
+ align="flex-start"
+ fillWidth
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => {
+ InkStrokeProperties.Instance.smoothInkStrokes(this.containsInkDoc ? DocListCast(targetDoc.data) : [targetDoc], this.smoothAmt);
+ }, 'smoothStrokes')}
+ />
+ </div>
+ <div className="smooth-slider">
+ {this.getNumber(
+ 'Smooth Amount',
+ '',
+ 1,
+ Math.max(20, this.smoothAmt),
+ this.smoothAmt,
+ (val: number) => {
+ !isNaN(val) && (this.smoothAmt = val);
+ },
+ 20,
+ 1
+ )}
+ </div>
+ {!targetDoc.layout_isSvg && (
+ <div className="color">
+ <Toggle
+ text={'Color with GPT'}
+ color={SettingsManager.userColor}
+ icon={<FontAwesomeIcon icon="fill-drip" />}
+ iconPlacement="left"
+ align="flex-start"
+ fillWidth
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => {
+ SmartDrawHandler.Instance.colorWithGPT(targetDoc);
+ }, 'smoothStrokes')}
+ />
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ @computed get dashdStk() { return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
set dashdStk(value) {
value && (this._lastDash = value);
- this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+ if (this.containsInkDoc) {
+ const childDocs = DocListCast(this.selectedDoc[DocData].data);
+ childDocs.forEach(doc => {
+ doc[DocData].stroke_dash = value ? this._lastDash : undefined;
+ });
+ } else {
+ this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+ }
}
@computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
set widthStk(value) {
@@ -930,13 +1028,31 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
set markScal(value) {
this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
}
+ @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '1'); } // prettier-ignore
+ set smoothAmt(value) {
+ this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value));
+ }
@computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
set markHead(value) {
- this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+ if (this.containsInkDoc) {
+ const childDocs = DocListCast(this.selectedDoc[DocData].data);
+ childDocs.forEach(doc => {
+ doc[DocData].stroke_startMarker = value;
+ });
+ } else {
+ this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+ }
}
@computed get markTail() { return this.getField('stroke_endMarker') || ''; } // prettier-ignore
set markTail(value) {
- this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+ if (this.containsInkDoc) {
+ const childDocs = DocListCast(this.selectedDoc[DocData].data);
+ childDocs.forEach(doc => {
+ doc[DocData].stroke_endMarker = value;
+ });
+ } else {
+ this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+ }
}
regInput = (key: string, value: any, setter: (val: string) => {}) => (
@@ -1036,6 +1152,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="appearance-editor">
{this.widthAndDash}
{this.strokeAndFill}
+ {this.smoothAndColor}
+ </div>
+ );
+ }
+
+ @computed get inkEditor() {
+ return (
+ <div className="ink-editor">
+ {this.widthAndDash}
+ {this.strokeAndFill}
</div>
);
}
@@ -1164,6 +1290,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
}
@computed get inkSubMenu() {
+ this.containsInkDoc = false;
return (
// prettier-ignore
<>
@@ -1177,6 +1304,30 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
);
}
+ containsInk = (selectedDoc: Doc) => {
+ const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data);
+ for (var i = 0; i < childDocs.length; i++) {
+ if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) {
+ this.inkDoc = childDocs[i];
+ this.containsInkDoc = true;
+ return true;
+ }
+ }
+ this.containsInkDoc = false;
+ return false;
+ };
+
+ @computed get inkCollectionSubMenu() {
+ return (
+ // prettier-ignore
+ <>
+ <PropertiesSection title="Ink Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
+ {this.isGroup && this.containsInk(this.selectedDoc) ? this.appearanceEditor : null}
+ </PropertiesSection>
+ </>
+ );
+ }
+
@computed get fieldsSubMenu() {
return (
<PropertiesSection
@@ -1737,6 +1888,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
{this.linksSubMenu}
{!this.selectedLink || !this.openLinks ? null : this.linkProperties}
{this.inkSubMenu}
+ {this.inkCollectionSubMenu}
{this.contextsSubMenu}
{isNovice ? null : this.sharingSubMenu}
{this.filtersSubMenu}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a298e6ac4..8575807b3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -548,24 +548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
default: {
const { points } = ge;
const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
- const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
- const inkDoc = Docs.Create.InkDocument(
- points,
- { title: ge.gesture.toString(),
- x: B.x - inkWidth / 2,
- y: B.y - inkWidth / 2,
- _width: B.width + inkWidth,
- _height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
- inkWidth,
- ActiveInkColor(),
- ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
- ActiveIsInkMask()
- );
+ const inkDoc = this.createInkDoc(points, B);
if (Doc.ActiveTool === InkTool.Write) {
this.unprocessedDocs.push(inkDoc);
CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
@@ -652,26 +635,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
const newStrokes = segments?.map(segment => {
const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
- const bounds = InkField.getBounds(points);
- const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
- const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
- return Docs.Create.InkDocument(
- points,
- { title: 'stroke',
- x: B.x - inkWidth / 2,
- y: B.y - inkWidth / 2,
- _width: B.width + inkWidth,
- _height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
- inkWidth,
- ActiveInkColor(),
- ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
- ActiveIsInkMask()
- );
+ return this.createInkDoc(points);
});
newStrokes && this.addDocument?.(newStrokes);
// setTimeout(() => this._eraserLock--);
@@ -1276,18 +1240,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return tVals;
};
+ createInkDoc = (points: InkData, transformedBounds?: { x: number; y: number; width: number; height: number }) => {
+ const bounds = InkField.getBounds(points);
+ const B = transformedBounds || this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+ return Docs.Create.InkDocument(
+ points,
+ { title: 'stroke',
+ x: B.x - inkWidth / 2,
+ y: B.y - inkWidth / 2,
+ _width: B.width + inkWidth,
+ _height: B.height + inkWidth,
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ inkWidth,
+ ActiveInkColor(),
+ ActiveInkBezierApprox(),
+ ActiveFillColor(),
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ };
+
@action
- showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => {
- SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing);
+ showSmartDraw = (e: PointerEvent) => {
+ SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+ SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
};
_drawing: Doc[] = [];
_drawingContainer: Doc | undefined = undefined;
@undoBatch
- createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
+ createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
this._drawing = [];
const xf = this.screenToFreeformContentsXf;
- // this._drawingContainer = undefined;
strokeData.forEach((stroke: [InkData, string, string]) => {
const bounds = InkField.getBounds(stroke[0]);
const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
@@ -1310,20 +1299,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
ActiveIsInkMask()
);
this._drawing.push(inkDoc);
- this.addDocument(inkDoc);
});
- const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
- if (collection) {
- const docData = collection[DocData];
- docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
- docData.drawingInput = opts.text;
- docData.drawingComplexity = opts.complexity;
- docData.drawingColored = opts.autoColor;
- docData.drawingSize = opts.size;
- docData.drawingData = gptRes;
- this._drawingContainer = collection;
- }
- this._batch?.end();
+ const collection = MarqueeView.getCollection(this._drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+ return collection;
};
removeDrawing = (doc?: Doc) => {
@@ -1339,6 +1317,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._drawing = [];
};
+ addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
+ const docData = doc[DocData];
+ docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ this._drawingContainer = doc;
+ this.addDocument(doc);
+ this._batch?.end();
+ };
+
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
@@ -2045,7 +2036,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
optionItems.push({
description: 'Show Drawing Editor',
event: action(() => {
- !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+ SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+ !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
}),
icon: 'pen-to-square',
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index b3fdd9379..76c37dff0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -21,6 +21,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public classifyImages: (e: React.MouseEvent | undefined) => void = unimplementedFunction;
public groupImages: () => void = unimplementedFunction;
+ public smoothStrokes: (docs?: Doc[]) => void = unimplementedFunction;
public isShown = () => this._opacity > 0;
constructor(props: any) {
super(props);
@@ -41,6 +42,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
<IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
<IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
<IconButton tooltip="Classify Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
+ <IconButton tooltip="Smooth Strokes" onPointerDown={() => this.smoothStrokes} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
</>
);
return this.getElement(buttons);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 8560323c9..92c0da983 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -4,14 +4,14 @@ import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils';
-import { intersectRect, numberRange } from '../../../../Utils';
+import { intersectRect, numberRange, unimplementedFunction } from '../../../../Utils';
import { Doc, NumListCast, Opt } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkData, InkField, InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { RichTextField } from '../../../../fields/RichTextField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { GetEffectiveAcl } from '../../../../fields/util';
import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT';
@@ -26,7 +26,7 @@ import { ContextMenu } from '../../ContextMenu';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { MarqueeViewBounds } from '../../PinFuncs';
import { PreviewCursor } from '../../PreviewCursor';
-import { DocumentView } from '../../nodes/DocumentView';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../../nodes/DocumentView';
import { OpenWhere } from '../../nodes/OpenWhere';
import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -36,7 +36,12 @@ import { CollectionFreeFormView } from './CollectionFreeFormView';
import { ImageLabelHandler } from './ImageLabelHandler';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
import './MarqueeView.scss';
-import { collectionOf } from '@turf/turf';
+import { collectionOf, points } from '@turf/turf';
+import { InkingStroke } from '../../InkingStroke';
+import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
+import { Gestures } from '../../../../pen-gestures/GestureTypes';
+import { GestureOverlay } from '../../GestureOverlay';
+import { InkStrokeProperties } from '../../InkStrokeProperties';
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -89,6 +94,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
return bounds;
}
+ public AddInkDoc: (points: InkData) => Doc | void = unimplementedFunction;
+
componentDidMount() {
this._props.setPreviewCursor?.(this.setPreviewCursor);
}
@@ -277,6 +284,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView;
MarqueeOptionsMenu.Instance.classifyImages = this.classifyImages;
MarqueeOptionsMenu.Instance.groupImages = this.groupImages;
+ MarqueeOptionsMenu.Instance.smoothStrokes = this.smoothStrokes;
document.addEventListener('pointerdown', hideMarquee, true);
document.addEventListener('wheel', hideMarquee, true);
} else {
@@ -490,6 +498,70 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
});
@undoBatch
+ smoothStrokes = action((docs?: Doc[]) => {
+ docs && docs.length > 0 ? (this._selectedDocs = docs) : (this._selectedDocs = this.marqueeSelect(false, DocumentType.INK));
+ if (this._selectedDocs.length == 0) return;
+
+ this._selectedDocs.forEach(stroke => {
+ const docView = DocumentView.getDocumentView(stroke);
+ const inkStroke = docView?.ComponentView as InkingStroke;
+ const { inkData } = inkStroke.inkScaledData();
+
+ const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
+ console.log(result);
+ let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
+ if (result && (result.Name === 'line' ? result.Score > 0.9 : result.Score > 0.8)) {
+ 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 distances: number[] = [];
+ for (var i = 0; i < inkData.length - 3; i += 4) {
+ distances.push(Math.sqrt((inkData[i].X - inkData[i + 3].X) ** 2 + (inkData[i].Y - inkData[i + 3].Y) ** 2));
+ }
+ const avgDist = (NumCast(stroke.width) + NumCast(stroke.height)) / 2;
+ // const avgDist = distances.reduce((a, b) => a + b) / distances.length;
+ if (Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) < avgDist) {
+ inkData.pop();
+ inkData.push({ X: inkData[0].X, Y: inkData[0].Y });
+ }
+ // const editedPoints: InkData = [];
+ // const toDelete: number[] = [];
+
+ // distances.forEach((dist, i) => {
+ // if (dist < avgDist / 3) {
+ // toDelete.unshift(i * 4);
+ // }
+ // });
+ // toDelete.forEach(pt => {
+ // InkStrokeProperties.Instance._currentPoint = pt;
+ // docView && InkStrokeProperties.Instance.deletePoints(docView, false);
+ // });
+
+ // for (var i = 0; i < distances.length; i++) {
+ // if (distances[i] > avgDist / 3) {
+ // editedPoints.push(...inkData.slice(i * 4, i * 4 + 4));
+ // } else {
+ // if (i !== distances.length) {
+ // editedPoints.push(...inkData.slice(i * 4, i * 4 + 2));
+ // editedPoints.push(...inkData.slice(i * 4 + 6, i * 4 + 8));
+ // i++;
+ // }
+ // }
+ // }
+ // inkData.length = 0;
+ // inkData.push(...editedPoints);
+ }
+ });
+ });
+
+ @undoBatch
syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
const selected = this.marqueeSelect(false);
if (e instanceof KeyboardEvent ? e.key === 'i' : true) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9f2a9b8e1..1e700d240 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -295,6 +295,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
});
+ AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
+ const container = DocCast(this._props.Document.embedContainer);
+ const docView = DocumentView.getDocumentView?.(container);
+ docView?.ComponentView?._props.addDocument?.(drawing);
+ drawing.x = NumCast(this._props.Document.x) + (this._props.Document.width as number);
+ drawing.y = NumCast(this._props.Document.y);
+ };
+
AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index df990b0c0..ea574493a 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -15,6 +15,11 @@ import { LinkPopup } from '../linking/LinkPopup';
import { DocumentView } from '../nodes/DocumentView';
import './AnchorMenu.scss';
import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
+import { InkData, InkField } from '../../../fields/InkField';
+import { DocData } from '../../../fields/DocSymbols';
+import { undoBatch } from '../../util/UndoManager';
+import ReactLoading from 'react-loading';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -38,6 +43,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
// GPT additions
@observable private _selectedText: string = '';
+ @observable private _isLoading: boolean = false;
@action
public setSelectedText = (txt: string) => {
this._selectedText = txt.trim();
@@ -61,6 +67,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public get Active() {
return this._left > 0;
}
+ public AddDrawingAnnotation: (doc: Doc) => void = unimplementedFunction;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
componentWillUnmount() {
@@ -137,6 +144,29 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.addToCollection?.(newCol);
};
+ gptDraw = async (e: React.PointerEvent) => {
+ try {
+ SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
+ this._isLoading = true;
+ await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
+ this._isLoading = false;
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ @undoBatch
+ createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+ this.AddDrawingAnnotation(drawing);
+ const docData = drawing[DocData];
+ docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ });
+
pointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -239,6 +269,14 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
icon={<FontAwesomeIcon icon="id-card" size="lg" />}
color={SettingsManager.userColor}
/>
+ {this._selectedText && (
+ <IconButton
+ tooltip="Create drawing"
+ onPointerDown={e => this.gptDraw(e)}
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon="paintbrush" size="lg" />}
+ color={SettingsManager.userColor}
+ />
+ )}
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
<IconButton
tooltip="Click to Record Annotation" //
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index fbe3518ec..5d526c1bf 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -33,7 +33,7 @@ import './PDFViewer.scss';
// pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
// The workerSrc property shall be specified.
-Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.3.136/build/pdf.worker.mjs';
+Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.mjs';
interface IViewerProps extends FieldViewProps {
pdfBox: PDFBox;
@@ -435,6 +435,14 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
// allows for creating collection
AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
+ AnchorMenu.Instance.AddDrawingAnnotation = this.addDrawingAnnotation;
+ };
+
+ addDrawingAnnotation = (drawing: Doc) => {
+ // drawing[DocData].x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX
+ // const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
+ drawing.y = (drawing.y as number) + (this._props.Document.data_sidebar_panY as number);
+ this._props.addDocument?.(drawing);
};
@action
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index ec4279e3e..7e4d46204 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -64,7 +64,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
@action
handleKeyPress = async (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
- await this.generateDrawing();
+ await this.generateDrawings();
}
};
@@ -130,15 +130,14 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
}
@undoBatch
- generateDrawing = action(async () => {
+ generateDrawings = action(async () => {
this._isLoading = true;
this._props.Document[DocData].data = undefined;
for (var i = 0; i < 3; i++) {
try {
- SmartDrawHandler.Instance._addFunc = this.createDrawing;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
this._canInteract = false;
if (this._showRegenerate) {
- SmartDrawHandler.Instance._deleteFunc = unimplementedFunction;
await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
} else {
await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
@@ -154,39 +153,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
});
@action
- createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
- this._opts = opts;
+ addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
this._gptRes.push(gptRes);
- const drawing: Doc[] = [];
-
- strokeList.forEach((stroke: [InkData, string, string]) => {
- const bounds = InkField.getBounds(stroke[0]);
- const inkWidth = Math.min(5, ActiveInkWidth());
- const inkDoc = Docs.Create.InkDocument(
- stroke[0],
- { title: 'stroke',
- x: bounds.left - inkWidth / 2,
- y: bounds.top - inkWidth / 2,
- _width: bounds.width + inkWidth,
- _height: bounds.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
- inkWidth,
- opts.autoColor ? stroke[1] : ActiveInkColor(),
- ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
- ActiveIsInkMask()
- );
- drawing.push(inkDoc);
- });
-
- const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
- if (collection) {
- collection[DocData].freeform_fitContentsToBox = true;
- Doc.AddDocToList(this._props.Document, 'data', collection);
- }
+ drawing[DocData].freeform_fitContentsToBox = true;
+ Doc.AddDocToList(this._props.Document, 'data', drawing);
};
saveDrawing = async () => {
@@ -262,7 +232,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
iconPlacement="right"
color={SettingsManager.userColor}
- onClick={this.generateDrawing}
+ onClick={this.generateDrawings}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}>
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index c842551c3..52df598ee 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -7,17 +7,23 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
import { Button, IconButton } from 'browndash-components';
import ReactLoading from 'react-loading';
import { AiOutlineSend } from 'react-icons/ai';
-import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
-import { InkData, InkTool } from '../../../fields/InkField';
+import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
+import { InkData, InkField, InkTool } from '../../../fields/InkField';
import { SVGToBezier } from '../../util/bezierFit';
const { parse } = require('svgson');
import { Slider, Switch } from '@mui/material';
-import { Doc } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
-import { DocumentView } from '../nodes/DocumentView';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import './SmartDrawHandler.scss';
import { unimplementedFunction } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { ImageField, URLField } from '../../../fields/URLField';
+import { CollectionCardView } from '../collections/CollectionCardDeckView';
+import { InkingStroke } from '../InkingStroke';
+import { undoBatch } from '../../util/UndoManager';
export interface DrawingOptions {
text: string;
@@ -46,13 +52,43 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
@observable private _autoColor: boolean = true;
@observable private _regenInput: string = '';
@observable private _canInteract: boolean = true;
- public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
- public _deleteFunc: (doc?: Doc) => void = () => {};
+
private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
private _lastResponse: string = '';
private _selectedDoc: Doc | undefined = undefined;
private _errorOccurredOnce = false;
+ public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction;
+ public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+ const drawing: Doc[] = [];
+ strokeList.forEach((stroke: [InkData, string, string]) => {
+ const bounds = InkField.getBounds(stroke[0]);
+ const inkWidth = Math.min(5, ActiveInkWidth());
+ const inkDoc = Docs.Create.InkDocument(
+ stroke[0],
+ { title: 'stroke',
+ x: bounds.left - inkWidth / 2,
+ y: bounds.top - inkWidth / 2,
+ _width: bounds.width + inkWidth,
+ _height: bounds.height + inkWidth,
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ inkWidth,
+ opts.autoColor ? stroke[1] : ActiveInkColor(),
+ ActiveInkBezierApprox(),
+ stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ drawing.push(inkDoc);
+ });
+
+ const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+ return collection;
+ };
+ public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
+
constructor(props: any) {
super(props);
makeObservable(this);
@@ -90,25 +126,26 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
};
@action
- displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+ setEdit = () => {
+ this._showEditBox = !this._showEditBox;
+ };
+
+ @action
+ displaySmartDrawHandler = (x: number, y: number) => {
this._pageX = x;
this._pageY = y;
this._display = true;
- this._addFunc = addFunc;
- this._deleteFunc = deleteFunc;
};
@action
- displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+ displayRegenerate = (x: number, y: number) => {
this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
- const docData = this._selectedDoc[DocData];
- this._addFunc = addFunc;
- this._deleteFunc = deleteFunc;
this._pageX = x;
this._pageY = y;
this._display = false;
this._showRegenerate = true;
this._showEditBox = false;
+ const docData = this._selectedDoc[DocData];
this._lastResponse = StrCast(docData.drawingData);
this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
};
@@ -155,8 +192,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
this._showOptions = false;
try {
await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
- this._showRegenerate = true;
this.hideSmartDrawHandler();
+ this._showRegenerate = true;
} catch (err) {
if (this._errorOccurredOnce) {
console.error('GPT call failed', err);
@@ -181,17 +218,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
return;
}
console.log(res);
- const strokeData = await this.parseResponse(res, startPt, false, autoColor);
+ const strokeData = await this.parseSvg(res, startPt, false, autoColor);
+ const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+
this._errorOccurredOnce = false;
return strokeData;
};
@action
- edit = () => {
- this._showEditBox = !this._showEditBox;
- };
-
- @action
regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
if (lastInput) this._lastInput = lastInput;
if (lastResponse) this._lastResponse = lastResponse;
@@ -211,14 +246,18 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
return;
}
console.log(res);
- await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+ 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);
+ const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+ return strokeData;
} catch (err) {
- console.error('GPT call failed', err);
+ console.error('Error regenerating drawing', err);
}
};
@action
- parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
+ parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
if (svg) {
this._lastResponse = svg[0];
@@ -235,16 +274,70 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
(regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
]);
});
- if (regenerate) {
- if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
- this._addFunc(strokeData, this._lastInput, svg[0]);
- } else {
- this._addFunc(strokeData, this._lastInput, svg[0]);
- }
return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
}
};
+ colorWithGPT = async (drawing: Doc) => {
+ const img = await this.getIcon(drawing);
+ const { href } = (img as URLField).url;
+ const hrefParts = href.split('.');
+ const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+ try {
+ const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
+ const strokes = DocListCast(drawing[DocData].data);
+ const coords: string[] = [];
+ strokes.forEach((stroke, i) => {
+ const inkingStroke = DocumentView.getDocumentView(stroke)?.ComponentView as InkingStroke;
+ const { inkData } = inkingStroke.inkScaledData();
+ coords.push(
+ `${i + 1}. ${inkData
+ .filter((point, index) => {
+ return index % 4 === 0 || index == inkData.length - 1;
+ })
+ .map(point => {
+ return `(${point.X.toString()}, ${point.Y.toString()})`;
+ })}`
+ );
+ });
+ const response = await gptDrawingColor(hrefBase64, coords);
+ console.log(response);
+ const colorResponse = await gptAPICall(response, GPTCallType.COLOR, undefined);
+ console.log(colorResponse);
+ this.colorStrokes(colorResponse, drawing);
+ } catch (error) {
+ console.log('GPT call failed');
+ }
+ };
+
+ @undoBatch
+ colorStrokes = (res: string, drawing: Doc) => {
+ const colorList = res.match(/\{.*?\}/g);
+ const strokes = DocListCast(drawing[DocData].data);
+ colorList?.forEach((colors, index) => {
+ const strokeAndFill = colors.match(/#[0-9A-Fa-f]{6}/g);
+ if (strokeAndFill && strokeAndFill.length == 2) {
+ strokes[index][DocData].color = strokeAndFill[0];
+ const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
+ const { inkData } = inkStroke.inkScaledData();
+ if (InkingStroke.IsClosed(inkData)) {
+ strokes[index][DocData].fillColor = strokeAndFill[1];
+ }
+ }
+ });
+ };
+
+ async getIcon(doc: Doc) {
+ const docView = DocumentView.getDocumentView(doc);
+ console.log(doc);
+ if (docView) {
+ console.log(docView);
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+
render() {
if (this._display) {
return (
@@ -397,7 +490,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
color={SettingsManager.userColor}
onClick={this.handleSendClick}
/>
- <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
+ <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.setEdit} />
{this._showEditBox && (
<div
style={{