diff options
author | bobzel <zzzman@gmail.com> | 2025-03-04 00:52:53 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-03-04 00:52:53 -0500 |
commit | 215ad40efa2e343e290d18bffbc55884829f1a0d (patch) | |
tree | 2e4e3310aad1ea5b39a874ecbc98efb1312bd21b | |
parent | 0e7ae057264445ece675e4b5d2380893ea124112 (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.ts | 57 | ||||
-rw-r--r-- | src/client/util/bezierFit.ts | 302 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 17 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 36 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 2 | ||||
-rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.tsx | 72 |
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} |