aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-03-04 00:52:53 -0500
committerbobzel <zzzman@gmail.com>2025-03-04 00:52:53 -0500
commit215ad40efa2e343e290d18bffbc55884829f1a0d (patch)
tree2e4e3310aad1ea5b39a874ecbc98efb1312bd21b
parent0e7ae057264445ece675e4b5d2380893ea124112 (diff)
fixed up smartDrawHandler a bit to support svg's better. you can now drop in a .svg file from the filesystem - still some unfinished business (arcs, background/foreground color inversion)
-rw-r--r--src/client/documents/DocUtils.ts57
-rw-r--r--src/client/util/bezierFit.ts302
-rw-r--r--src/client/views/collections/CollectionSubView.tsx17
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx36
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx2
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx72
6 files changed, 363 insertions, 123 deletions
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 18c8d97d4..7f22e9376 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -10,7 +10,7 @@ import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, Field, FieldResult, FieldType, LinkedTo, Opt, StrListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
-import { InkDataFieldName, InkField } from '../../fields/InkField';
+import { InkData, InkDataFieldName, InkField } from '../../fields/InkField';
import { List, ListFieldName } from '../../fields/List';
import { ProxyField } from '../../fields/Proxy';
import { RichTextField } from '../../fields/RichTextField';
@@ -33,6 +33,10 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
import { DocumentType } from './DocumentTypes';
import { Docs, DocumentOptions } from './Documents';
import { DocumentView } from '../views/nodes/DocumentView';
+import { INode, parse } from 'svgson';
+import { SVGToBezier, SVGType } from '../util/bezierFit';
+import { SmartDrawHandler } from '../views/smartdraw/SmartDrawHandler';
+import { PointData } from '../../pen-gestures/GestureTypes';
export namespace DocUtils {
function HasFunctionFilter(val: string) {
@@ -780,6 +784,57 @@ export namespace DocUtils {
return generatedDocuments;
}
+ export async function openSVGfile(file: File, options: DocumentOptions) {
+ const reader = new FileReader();
+ const scale = 1;
+ const startPoint = { X: (options.x as number) ?? 0, Y: (options.y as number) ?? 0 };
+ const buffer = await new Promise<string>((res, rej) => {
+ reader.onload = event => {
+ const fileContent = event.target?.result;
+ // Process the file content here
+ console.log(fileContent);
+ typeof fileContent === 'string' ? res(fileContent) : rej();
+ };
+
+ reader.readAsText(file);
+ });
+ const svg = buffer.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ if (svg) {
+ const svgObject = await parse(svg[0]);
+ const strokeData: [InkData, string, string][] = [];
+ const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER };
+ let last: PointData = { X: 0, Y: 0 };
+ const processStroke = (child: INode) => {
+ child.attributes.d
+ .split(/[\n]?M/)
+ .slice(1)
+ .map((d, ind) => {
+ const convertedBezier: InkData = SVGToBezier(child.name as SVGType, { ...child, d: '\nM' + d } as unknown as Record<string, string>, last);
+ last = convertedBezier.lastElement();
+ convertedBezier.forEach(point => {
+ if (point.X < tl.X) tl.X = point.X;
+ if (point.Y < tl.Y) tl.Y = point.Y;
+ });
+ strokeData.push([convertedBezier, child.attributes.stroke || 'black', ind === 0 ? child.attributes.fill : child.attributes.fill === 'none' ? child.attributes.fill : DashColor(child.attributes.fill).negate().toString()]);
+ });
+ };
+ const processNode = (parent: INode) => {
+ if (parent.children.length) parent.children.forEach(processNode);
+ else if (parent.type !== 'text') processStroke(parent);
+ };
+ processNode(svgObject);
+
+ const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * scale, Y: startPoint.Y + (pd.Y - tl.Y) * scale });
+
+ return SmartDrawHandler.CreateDrawingDoc(
+ strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as [PointData[], string, string]),
+ { autoColor: true },
+ '',
+ undefined
+ );
+ }
+ }
+
export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
const generatedDocuments: Doc[] = [];
// Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid.
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 7ef370d48..65bd44bf9 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -1,7 +1,5 @@
/* eslint-disable no-use-before-define */
-/* eslint-disable prefer-destructuring */
/* eslint-disable no-param-reassign */
-/* eslint-disable camelcase */
import { Point } from '../../pen-gestures/ndollar';
export enum SVGType {
@@ -625,13 +623,130 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
return [...firstEnd, ...points, ...lastEnd];
}
-export function SVGToBezier(name: SVGType, attributes: any): Point[] {
+function convertToAbsolute(pathData: string): string {
+ const commands = pathData.match(/[a-zA-Z][^a-zA-Z]*/g);
+ if (!commands) return pathData;
+
+ let currentX = 0;
+ let currentY = 0;
+ let startX = 0;
+ let startY = 0;
+
+ const absoluteCommands = commands.map(command => {
+ const type = command[0];
+ const values = command
+ .slice(1)
+ .trim()
+ .split(/[\s,]+/)
+ .map(v => +v);
+
+ switch (type) {
+ case 'M':
+ currentX = values[0];
+ currentY = values[1];
+ startX = currentX;
+ startY = currentY;
+ return `M${currentX},${currentY}`;
+ case 'm':
+ currentX += values[0];
+ currentY += values[1];
+ startX = currentX;
+ startY = currentY;
+ return `M${currentX},${currentY}`;
+ case 'L':
+ currentX = values[0];
+ currentY = values[1];
+ return `L${currentX},${currentY}`;
+ case 'l':
+ currentX += values[0];
+ currentY += values[1];
+ return `L${currentX},${currentY}`;
+ case 'H':
+ currentX = values[0];
+ return `H${currentX}`;
+ case 'h':
+ currentX += values[0];
+ return `H${currentX}`;
+ case 'V':
+ currentY = values[0];
+ return `V${currentY}`;
+ case 'v':
+ currentY += values[0];
+ return `V${currentY}`;
+ case 'C':
+ currentX = values[4];
+ currentY = values[5];
+ return `C${values.join(',')}`;
+ case 'c': {
+ let str = '';
+ for (let i = 0; i < values.length; i += 6) {
+ str += (i === 0 ? 'C':',') + (values[i] + currentX) +
+ ',' + (values[i + 1] + currentY) +
+ ',' + (values[i + 2] + currentX) +
+ ',' + (values[i + 3] + currentY) +
+ ',' + (values[i + 4] + currentX) +
+ ',' + (values[i + 5] + currentY); // prettier-ignore
+ currentX += values[i + 4];
+ currentY += values[i + 5];
+ }
+ return str;
+ }
+ case 'S':
+ currentX = values[2];
+ currentY = values[3];
+ return `S${values.join(',')}`;
+ case 's':
+ return `S${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`;
+ case 'Q':
+ currentX = values[2];
+ currentY = values[3];
+ return `Q${values.join(',')}`;
+ case 'q': {
+ let str = '';
+ for (let i = 0; i < values.length; i += 4) {
+ str += (i === 0 ? 'Q':',') + (values[i] + currentX) +
+ ',' + (values[i + 1] + currentY) +
+ ',' + (values[i + 2] + currentX) +
+ ',' + (values[i + 3] + currentY); // prettier-ignore
+ currentX += values[i + 2];
+ currentY += values[i + 3];
+ }
+ return str;
+ }
+ case 'T':
+ currentX = values[0];
+ currentY = values[1];
+ return `T${currentX},${currentY}`;
+ case 't':
+ currentX += values[0];
+ currentY += values[1];
+ return `T${currentX},${currentY}`;
+ case 'A':
+ currentX = values[5];
+ currentY = values[6];
+ return `A${values.join(',')}`;
+ case 'a':
+ return `A${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`;
+ case 'Z':
+ case 'z':
+ currentX = startX;
+ currentY = startY;
+ return 'Z';
+ default:
+ return command;
+ }
+ });
+
+ return absoluteCommands.join(' ');
+}
+
+export function SVGToBezier(name: SVGType, attributes: Record<string, string>, last: { X: number; Y: number }): Point[] {
switch (name) {
case 'line': {
- const x1 = parseInt(attributes.x1);
- const x2 = parseInt(attributes.x2);
- const y1 = parseInt(attributes.y1);
- const y2 = parseInt(attributes.y2);
+ const x1 = +attributes.x1;
+ const x2 = +attributes.x2;
+ const y1 = +attributes.y1;
+ const y2 = +attributes.y2;
return [
{ X: x1, Y: y1 },
{ X: x1, Y: y1 },
@@ -642,10 +757,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
case 'circle':
case 'ellipse': {
const c = 0.551915024494;
- const centerX = parseInt(attributes.cx);
- const centerY = parseInt(attributes.cy);
- const radiusX = parseInt(attributes.rx) || parseInt(attributes.r);
- const radiusY = parseInt(attributes.ry) || parseInt(attributes.r);
+ const centerX = +attributes.cx;
+ const centerY = +attributes.cy;
+ const radiusX = +attributes.rx || +attributes.r;
+ const radiusY = +attributes.ry || +attributes.r;
return [
{ X: centerX, Y: centerY + radiusY },
{ X: centerX + c * radiusX, Y: centerY + radiusY },
@@ -666,10 +781,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
];
}
case 'rect': {
- const x = parseInt(attributes.x);
- const y = parseInt(attributes.y);
- const width = parseInt(attributes.width);
- const height = parseInt(attributes.height);
+ const x = +attributes.x;
+ const y = +attributes.y;
+ const width = +attributes.width;
+ const height = +attributes.height;
return [
{ X: x, Y: y },
{ X: x, Y: y },
@@ -691,41 +806,122 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
}
case 'path': {
const coordList: Point[] = [];
- const [startX, startY] = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/).slice(1);
- const startPt = { X: parseInt(startX), Y: parseInt(startY) };
- coordList.push(startPt);
- 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 = startPt;
- matches.forEach(match => {
- if (match[0].startsWith('Q')) {
- coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
- coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
- coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
- coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
- lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) };
- } else if (match[0].startsWith('C')) {
- coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) });
- coordList.push({ X: parseInt(match[7]), Y: parseInt(match[8]) });
- coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
- coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
- lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) };
- } else {
- 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]) });
- lastPt = { X: parseInt(match[11]), Y: parseInt(match[12]) };
+ let fixedattrs = attributes.d.trim().replace(/([0-9])-/g, '$1,-');
+ for (let i = 0; i < 100; i++) {
+ const test = fixedattrs.replace(/([0-9]?\.[0-9]+)(\.[0-9]+)/g, '$1,$2');
+ if (test === fixedattrs) break;
+ fixedattrs = test;
+ }
+ const attrdata = convertToAbsolute(fixedattrs);
+ const move = attrdata.match(/M(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+ const [startX, startY] = move?.slice(1) ?? [last.X + '', last.Y + ''];
+ const startPt = { X: +startX, Y: +startY };
+ let first = true;
+ let lastCmd = '';
+ for (let attr = attrdata.slice(move?.[0].length ?? 0).trim(); attr; ) {
+ lastCmd = 'AQCLVHZ'.includes(attr[0]) ? attr[0] : lastCmd;
+ switch (lastCmd) {
+ case 'Q': {
+ const match = attr.match(/Q?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+ if (match) {
+ const prev = first ? startPt : coordList.lastElement();
+ const Q = [+match[1], +match[2], +match[3], +match[4]];
+
+ coordList.push(prev);
+ coordList.push({ X: prev.X + (2 / 3) * (Q[0] - prev.X), Y: prev.Y + (2 / 3) * (Q[1] - prev.Y) });
+ coordList.push({ X: Q[2] + (2 / 3) * (Q[0] - Q[2]), Y: Q[3] + (2 / 3) * (Q[1] - Q[3]) });
+ coordList.push({ X: Q[2], Y: Q[3] });
+ attr = attr.slice(match[0].length).trim();
+ } else {
+ attr = attr.slice(1).trim();
+ alert('error' + attr);
+ }
+ break;
+ }
+ case 'C': {
+ const match = attr.match(/C?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+ if (match) {
+ coordList.push(first ? startPt : coordList.lastElement());
+ coordList.push({ X: +match[1], Y: +match[2] });
+ coordList.push({ X: +match[3], Y: +match[4] });
+ coordList.push({ X: +match[5], Y: +match[6] });
+ attr = attr.slice(match[0].length).trim();
+ } else {
+ attr = attr.slice(1).trim();
+ alert('error' + attr);
+ }
+ break;
+ }
+ case 'A': {
+ const match = attr.match(/A?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+ if (match) {
+ console.log('SKIPPING arc - not implemented');
+ // coordList.push(first ? startPt : coordList.lastElement());
+ // coordList.push({ X: +match[1], Y: +match[2] });
+ // coordList.push({ X: +match[3], Y: +match[4] });
+ // coordList.push({ X: +match[5], Y: +match[6] });
+ attr = attr.slice(match[0].length).trim();
+ } else {
+ attr = attr.slice(1).trim();
+ alert('error' + attr);
+ }
+ break;
+ }
+ case 'L': {
+ const match = attr.match(/L?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+ if (match) {
+ coordList.push(first ? startPt : coordList.lastElement());
+ coordList.push(coordList.lastElement());
+ coordList.push({ X: +match[1], Y: +match[2] });
+ coordList.push({ X: +match[1], Y: +match[2] });
+ attr = attr.slice(match[0].length).trim();
+ } else {
+ attr = attr.slice(1).trim();
+ alert('error' + attr);
+ }
+ break;
+ }
+ case 'H': {
+ const match = attr.match(/H?[, ]?(-?\d*\.?\d*)/);
+ if (match) {
+ coordList.push(first ? startPt : coordList.lastElement());
+ coordList.push(coordList.lastElement());
+ coordList.push({ X: +match[1], Y: coordList.lastElement().Y });
+ coordList.push({ X: +match[1], Y: coordList.lastElement().Y });
+ attr = attr.slice(match[0].length).trim();
+ } else {
+ attr = attr.slice(1).trim();
+ alert('error' + attr);
+ }
+ break;
+ }
+ case 'V': {
+ const match = attr.match(/V?[, ]?(-?\d*\.?\d*)/);
+ if (match) {
+ coordList.push(first ? startPt : coordList.lastElement());
+ coordList.push(coordList.lastElement());
+ coordList.push({ X: coordList.lastElement().X, Y: +match[1] });
+ coordList.push({ X: coordList.lastElement().X, Y: +match[1] });
+ attr = attr.slice(match[0].length).trim();
+ } else {
+ attr = attr.slice(1).trim();
+ alert('error' + attr);
+ }
+ break;
+ }
+ case 'Z': {
+ coordList.push(first ? startPt : coordList.lastElement());
+ coordList.push(first ? startPt : coordList.lastElement());
+ coordList.push(startPt);
+ coordList.push(startPt);
+ attr = attr.slice(1).trim();
+ break;
+ }
+ default:
+ attr = attr.slice(1).trim();
+ debugger;
}
- });
- const hasZ = attributes.d.match(/Z/);
- if (hasZ || attributes.fill) {
- coordList.push(lastPt);
- coordList.push(startPt);
- coordList.push(startPt);
- } else {
- coordList.pop();
+ first = false;
}
return coordList;
}
@@ -733,10 +929,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
let list: Point[] = [];
coords.forEach(coord => {
- list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
- list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
- list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
- list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
+ list.push({ X: +coord[1], Y: +coord[2] });
+ list.push({ X: +coord[1], Y: +coord[2] });
+ list.push({ X: +coord[1], Y: +coord[2] });
+ list.push({ X: +coord[1], Y: +coord[2] });
});
const firstPts = list.splice(0, 2);
list = list.concat(firstPts);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index ca830aa6f..655894e40 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -541,12 +541,17 @@ export function CollectionSubView<X>() {
DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
} else {
generatedDocuments.push(
- ...files.map(file => {
- const loading = Docs.Create.LoadingDocument(file, options);
- Doc.addCurrentlyLoading(loading);
- DocUtils.uploadFileToDoc(file, {}, loading);
- return loading;
- })
+ ...(await Promise.all(
+ files.map(async file => {
+ if (file.name.endsWith('svg')) {
+ return (await DocUtils.openSVGfile(file, options)) as Doc;
+ }
+ const loading = Docs.Create.LoadingDocument(file, options);
+ Doc.addCurrentlyLoading(loading);
+ DocUtils.uploadFileToDoc(file, {}, loading);
+ return loading;
+ })
+ ))
);
}
if (generatedDocuments.length) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aa9b9b0fa..b3d908da4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -30,7 +30,7 @@ import { CompileScript } from '../../../util/Scripting';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
-import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
+import { undoable, UndoManager } from '../../../util/UndoManager';
import { Timeline } from '../../animationtimeline/Timeline';
import { ContextMenu } from '../../ContextMenu';
import { InkingStroke } from '../../InkingStroke';
@@ -1232,7 +1232,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
showSmartDraw = (x: number, y: number, regenerate?: boolean) => {
const sm = SmartDrawHandler.Instance;
- sm.CreateDrawingDoc = this.createDrawingDoc;
sm.RemoveDrawing = this.removeDrawing;
sm.AddDrawing = this.addDrawing;
(regenerate ? sm.displayRegenerate : sm.displaySmartDrawHandler)(x, y, NumCast(this.layoutDoc[this.scaleFieldKey]));
@@ -1240,38 +1239,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
_drawing: Doc[] = [];
_drawingContainer: Doc | undefined = undefined;
- /**
- * Function that creates a drawing--a group of ink strokes--to go with the smart draw function.
- */
- @undoBatch
- createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions) => {
- this._drawing = [];
- const xf = this.screenToFreeformContentsXf;
- strokeData.forEach((stroke: [InkData, string, string]) => {
- const bounds = InkField.getBounds(stroke[0]);
- const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
- const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
- const inkDoc = Docs.Create.InkDocument(
- stroke[0],
- { 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().activeHideTextLabels)}, // prettier-ignore
- inkWidth,
- opts.autoColor ? stroke[1] : ActiveInkColor(),
- ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
- ActiveInkArrowStart(),
- ActiveInkArrowEnd(),
- ActiveInkDash(),
- ActiveIsInkMask()
- );
- this._drawing.push(inkDoc);
- });
- return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 });
- };
/**
* Part of regenerating a drawing--deletes the old drawing.
@@ -2007,7 +1974,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
optionItems.push({
description: 'Regenerate AI Drawing',
event: action(() => {
- 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();
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index f7070c780..28371594e 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -139,7 +139,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
this.AddDrawingAnnotation(drawing);
const docData = drawing[DocData];
- docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.title = opts.text?.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
docData.ai_drawing_input = opts.text;
docData.ai_drawing_complexity = opts.complexity;
docData.ai_drawing_colored = opts.autoColor;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index fbf471900..ca308015d 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -27,16 +27,19 @@ import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveIn
import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from './FireflyConstants';
import './SmartDrawHandler.scss';
import { Upload } from '../../../server/SharedMediaTypes';
+import { PointData } from '../../../pen-gestures/GestureTypes';
export interface DrawingOptions {
- text: string;
- complexity: number;
- size: number;
- autoColor: boolean;
- x: number;
- y: number;
+ text?: string;
+ complexity?: number;
+ size?: number;
+ autoColor?: boolean;
+ x?: number;
+ y?: number;
}
+type svgparsedData = [PointData[], string, string];
+
/**
* 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
@@ -102,7 +105,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
* 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) => {
+ public static 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]) => {
const bounds = InkField.getBounds(stroke[0]);
@@ -114,7 +117,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
y: bounds.top - inkWidth / 2,
_width: bounds.width + inkWidth,
_height: bounds.height + inkWidth,
- stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: false}, // prettier-ignore
inkWidth,
opts.autoColor ? stroke[1] : ActiveInkColor(),
ActiveInkBezierApprox(),
@@ -188,9 +191,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
/**
* This allows users to press the return/enter key to send input.
*/
- handleKeyPress = (event: React.KeyboardEvent) => {
- if (event.key === 'Enter') {
- this.handleSendClick();
+ handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ this.handleSendClick(this._pageX, this._pageY);
}
};
@@ -200,7 +203,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
* what the user sees.
*/
@action
- handleSendClick = async () => {
+ handleSendClick = async (X: number, Y: number) => {
if ((!this.ShowRegenerate && this._userInput == '') || (!this._generateImage && !this._generateDrawing)) return;
this._isLoading = true;
this._canInteract = false;
@@ -213,7 +216,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
await this.createImageWithFirefly(this._userInput);
}
if (this._generateDrawing) {
- await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ await this.drawWithGPT({ X, Y }, this._userInput, this._complexity, this._size, this._autoColor);
}
this.hideSmartDrawHandler();
} catch (err) {
@@ -229,14 +232,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
/**
* Calls GPT API to create a drawing based on user input.
*/
- drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
+ drawWithGPT = async (screenPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
if (input) {
- this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+ this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: screenPt.X, y: screenPt.Y };
const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
if (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);
+ const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, false, autoColor);
+ const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res, screenPt.X, screenPt.Y);
drawingDoc && this._selectedDocs.push(drawingDoc);
return strokeData;
} else {
@@ -334,9 +337,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
})();
if (res) {
- const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+ const strokeData = await this.parseSvg(res, { X: this._lastInput.x ?? 0, Y: this._lastInput.y ?? 0 }, true, lastInput?.autoColor || this._autoColor);
this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc);
- const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+ const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
} else {
console.error('GPT call failed');
@@ -356,20 +359,35 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
*/
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];
const svgObject = await parse(svg[0]);
+ console.log(res, svgObject);
const svgStrokes: INode[] = svgObject.children;
const strokeData: [InkData, string, string][] = [];
+
+ const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER };
+ let last: PointData = { X: 0, Y: 0 };
svgStrokes.forEach(child => {
- const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
+ const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes, last);
+ last = convertedBezier.lastElement();
strokeData.push([
- convertedBezier.map(point => ({ X: startPoint.X + (point.X - startPoint.X) * this._scale, Y: startPoint.Y + (point.Y - startPoint.Y) * this._scale })),
+ convertedBezier.map(point => {
+ if (point.X < tl.X) tl.X = point.X;
+ if (point.Y < tl.Y) tl.Y = point.Y;
+ return { X: point.X, Y: point.Y };
+ }),
(regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '',
(regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
]);
});
- return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
+ const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * this._scale, Y: startPoint.Y + (pd.Y - tl.Y) * this._scale });
+ return {
+ data: strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as svgparsedData),
+ lastInput: this._lastInput,
+ lastRes: svg[0],
+ };
}
};
@@ -439,7 +457,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
},
}}
checked={this._generateImage}
- onChange={() => this._canInteract && (this._generateImage = !this._generateImage)}
+ onChange={action(() => this._canInteract && (this._generateImage = !this._generateImage))}
/>
</div>
</div>
@@ -566,7 +584,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
iconPlacement="right"
color={SettingsManager.userColor}
- onClick={this.handleSendClick}
+ onClick={() => this.handleSendClick(this._pageX, this._pageY)}
/>
</div>
{this._showOptions && (
@@ -598,7 +616,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
iconPlacement="right"
color={SettingsManager.userColor}
- onClick={this.handleSendClick}
+ onClick={() => this.handleSendClick(this._pageX, this._pageY)}
/>
</div>
);
@@ -644,7 +662,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
tooltip="Regenerate"
icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
color={SettingsManager.userColor}
- onClick={this.handleSendClick}
+ onClick={() => this.handleSendClick(this._pageX, this._pageY)}
/>
<IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} />
{this._showEditBox ? this.renderRegenerateEditBox() : null}