From 4ab636e338a11e8153d43adddb0e0d3e6bad57ec Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 3 Nov 2024 18:12:54 -0500 Subject: fixed parsing of chat's svg's to strokes --- src/client/util/bezierFit.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src/client/util/bezierFit.ts') diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 4aef28e6b..d52460023 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -691,8 +691,9 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { } case 'path': { const coordList: Point[] = []; - const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/); - coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + const [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) ); @@ -721,8 +722,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { const hasZ = attributes.d.match(/Z/); if (hasZ) { coordList.push(lastPt); - coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); - coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + coordList.push(startPt); + coordList.push(startPt); } else { coordList.pop(); } -- cgit v1.2.3-70-g09d2 From 89424e0a8efc6cf3364a2fd1ffc85c9d0d837453 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 22 Nov 2024 10:27:33 -0500 Subject: added initial Firefly endpoint and hanged smartDrawHandler to generate an image and an svg. --- src/client/util/bezierFit.ts | 3 +- src/client/views/MainView.tsx | 32 ++++++++++++-- .../nodes/chatbot/chatboxcomponents/ChatBox.tsx | 3 +- src/client/views/pdf/AnchorMenu.tsx | 9 ++-- src/client/views/smartdraw/SmartDrawHandler.tsx | 29 ++++++------ src/server/ApiManagers/DataVizManager.ts | 2 +- src/server/ApiManagers/FireflyManager.ts | 51 ++++++++++++++++++++++ src/server/DashUploadUtils.ts | 3 +- src/server/index.ts | 3 +- webpack.config.js | 7 ++- 10 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 src/server/ApiManagers/FireflyManager.ts (limited to 'src/client/util/bezierFit.ts') diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index d52460023..84b27e84c 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -703,7 +703,6 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { 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]) }); @@ -720,7 +719,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { } }); const hasZ = attributes.d.match(/Z/); - if (hasZ) { + if (hasZ || attributes.fill) { coordList.push(lastPt); coordList.push(startPt); coordList.push(startPt); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7779d339f..0d071fe4f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import '../../../node_modules/browndash-components/dist/styles/global.min.css'; -import { ClientUtils, lightOrDark, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils'; +import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, GetDocFromUrl, Opt, returnEmptyDoclist } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; @@ -1023,10 +1023,36 @@ export class MainView extends ObservableReactComponent { {[ ...SnappingManager.HorizSnapLines.map(l => ( - + )), ...SnappingManager.VertSnapLines.map(l => ( - + )), ]} diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index a61705250..3ef6bdd8b 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -431,7 +431,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { doc = Docs.Create.FunctionPlotDocument([], options); break; case 'dataviz': - case 'data_viz': + case 'data_viz': { const { fileUrl, id } = await Networking.PostToServer('/createCSV', { filename: (options.title as string).replace(/\s+/g, '') + '.csv', data: data, @@ -439,6 +439,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { doc = Docs.Create.DataVizDocument(fileUrl, { ...options, text: RTFCast(data) }); this.addCSVForAnalysis(doc, id); break; + } case 'chat': doc = Docs.Create.ChatDocument(options); break; diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 5ab9b556c..fe03f32a5 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -131,12 +131,15 @@ export class AnchorMenu extends AntimodeMenu { /** * Creates a GPT drawing based on selected text. */ - gptDraw = async (e: React.PointerEvent) => { + gptDraw = (e: React.PointerEvent) => { try { SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation; runInAction(() => (this._isLoading = true)); - await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true); - runInAction(() => (this._isLoading = false)); + SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true)?.then( + action(() => { + this._isLoading = false; + }) + ); } catch (err) { console.error(err); } diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index d0f6566a5..342b91bd9 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -13,6 +13,7 @@ import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { InkData, InkField, InkTool } from '../../../fields/InkField'; import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { Networking } from '../../Network'; import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { SettingsManager } from '../../util/SettingsManager'; @@ -21,7 +22,8 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit'; import { InkingStroke } from '../InkingStroke'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { MarqueeView } from '../collections/collectionFreeForm'; -import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView'; +import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveInkColor, ActiveInkDash, ActiveInkFillColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { OpenWhere } from '../nodes/OpenWhere'; import './SmartDrawHandler.scss'; export interface DrawingOptions { @@ -230,20 +232,21 @@ export class SmartDrawHandler extends ObservableReactComponent { * Calls GPT API to create a drawing based on user input. */ @action - drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { - if (input === '') return; - this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; - const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); - if (!res) { - console.error('GPT call failed'); - return; - } - 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); + drawWithGPT = (startPt: { X: number; Y: number }, prompt: string, complexity: number, size: number, autoColor: boolean) => { + if (prompt === '') return; + this._lastInput = { text: prompt, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; + + Networking.PostToServer('/queryFireflyImage', { prompt }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, { title: prompt }), OpenWhere.addRight)); + + const result = gptAPICall(`"${prompt}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true).then(res => + this.parseSvg(res, startPt, false, autoColor).then(strokeData => { + const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + }) + ); this._errorOccurredOnce = false; - return strokeData; + return result; }; /** diff --git a/src/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts index 88f22992d..d2028f23b 100644 --- a/src/server/ApiManagers/DataVizManager.ts +++ b/src/server/ApiManagers/DataVizManager.ts @@ -9,7 +9,7 @@ export default class DataVizManager extends ApiManager { register({ method: Method.GET, subscription: '/csvData', - secureHandler: async ({ req, res }) => { + secureHandler: ({ req, res }) => { const uri = req.query.uri as string; return new Promise(resolve => { diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts new file mode 100644 index 000000000..04fa8f065 --- /dev/null +++ b/src/server/ApiManagers/FireflyManager.ts @@ -0,0 +1,51 @@ +import { DashUploadUtils } from '../DashUploadUtils'; +import { _invalid, _success, Method } from '../RouteManager'; +import ApiManager, { Registration } from './ApiManager'; + +export default class FireflyManager extends ApiManager { + askFirefly = (prompt: string = 'a realistic illustration of a cat coding') => { + const fetched = fetch('https://ims-na1.adobelogin.com/ims/token/v3', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `grant_type=client_credentials&client_id=${process.env._CLIENT_FIREFLY_CLIENT_ID}&client_secret=${process.env._CLIENT_FIREFLY_SECRET}&scope=openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis`, + }) + .then(response => response.json()) + .then((data: { access_token: string }) => + fetch('https://firefly-api.adobe.io/v3/images/generate', { + method: 'POST', + headers: [ + ['Content-Type', 'application/json'], + ['Accept', 'application/json'], + ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''], + ['Authorization', `Bearer ${data.access_token}`], + ], + body: `{ "prompt": "${prompt}" }`, + }) + .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image))) + .catch(error => { + console.error('Error:', error); + return ''; + }) + ) + .catch(error => { + console.error('Error:', error); + return ''; + }); + return fetched; + }; + protected initialize(register: Registration): void { + register({ + method: Method.POST, + subscription: '/queryFireflyImage', + secureHandler: ({ req, res }) => + this.askFirefly(req.body.prompt).then(fire => + DashUploadUtils.UploadImage(JSON.parse(fire).url).then(info => { + if (info instanceof Error) _invalid(res, info.message); + else _success(res, info.accessPaths.agnostic.client); + }) + ), + }); + } +} diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 1e55a885a..032d13d43 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -369,7 +369,8 @@ export namespace DashUploadUtils { */ export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename: string, prefix = '', cleanUp = true): Promise => { const { requestable, source, ...remaining } = metadata; - const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`; + const dfltSuffix = remaining.contentType.split('/')[1].toLowerCase(); + const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${dfltSuffix === 'xml' ? 'jpg' : dfltSuffix}`; const { images } = Directory; const information: Upload.ImageInformation = { accessPaths: { diff --git a/src/server/index.ts b/src/server/index.ts index 88dbd232d..1f9af9ee0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,6 +7,7 @@ import AssistantManager from './ApiManagers/AssistantManager'; import DataVizManager from './ApiManagers/DataVizManager'; import DeleteManager from './ApiManagers/DeleteManager'; import DownloadManager from './ApiManagers/DownloadManager'; +import FireflyManager from './ApiManagers/FireflyManager'; import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager'; import SessionManager from './ApiManagers/SessionManager'; import UploadManager from './ApiManagers/UploadManager'; @@ -71,6 +72,7 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage new GeneralGoogleManager(), /* new GooglePhotosManager(), */ new DataVizManager(), new AssistantManager(), + new FireflyManager(), ]; // initialize API Managers @@ -112,7 +114,6 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage }); const serve: PublicHandler = ({ req, res }) => { - // eslint-disable-next-line new-cap const detector = new mobileDetect(req.headers['user-agent'] || ''); const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; res.sendFile(path.join(__dirname, '../../deploy/' + filename)); diff --git a/webpack.config.js b/webpack.config.js index e1afc64e5..67417fb02 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -36,7 +36,6 @@ function transferEnvironmentVariables() { } const resolvedClientSide = Object.keys(parsed).reduce((mapping, envKey) => { if (envKey.startsWith(prefix)) { - // eslint-disable-next-line mapping[`process.env.${envKey.replace(prefix, '')}`] = JSON.stringify(parsed[envKey]); } return mapping; @@ -112,7 +111,7 @@ module.exports = { test: /\.scss|css$/, exclude: /\.module\.scss$/i, use: [ - { loader: 'style-loader' }, // eslint-disable-next-line prettier/prettier + { loader: 'style-loader' }, // { loader: 'css-loader' }, { loader: 'sass-loader' }, ], @@ -127,7 +126,7 @@ module.exports = { { test: /\.module\.scss$/i, use: [ - { loader: 'style-loader' }, // eslint-disable-next-line prettier/prettier + { loader: 'style-loader' }, // { loader: 'css-loader', options: { modules: true } }, { loader: 'sass-loader' }, ], -- cgit v1.2.3-70-g09d2 From 383b0487d5268bd860e514feddf09f4f3eb2fe3f Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Thu, 2 Jan 2025 01:13:50 -0500 Subject: made drawing fill automatically size images --- src/client/util/bezierFit.ts | 1 + .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 5 +- .../imageEditor/imageEditingUtils/ImageHandler.ts | 312 --------------------- src/client/views/smartdraw/DrawingFillHandler.tsx | 18 +- src/client/views/smartdraw/FireflyConstants.ts | 20 ++ src/client/views/smartdraw/SmartDrawHandler.scss | 3 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 34 +-- src/server/ApiManagers/FireflyManager.ts | 7 +- 9 files changed, 51 insertions(+), 351 deletions(-) delete mode 100644 src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts create mode 100644 src/client/views/smartdraw/FireflyConstants.ts (limited to 'src/client/util/bezierFit.ts') diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 84b27e84c..7ef370d48 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -703,6 +703,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { 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]) }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4bccdd286..ef0b80720 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1995,7 +1995,7 @@ export class CollectionFreeFormView extends CollectionSubView { SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; SmartDrawHandler.Instance.AddDrawing = this.addDrawing; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 8f6a90e61..7ce429f0f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -355,9 +355,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this.layoutDoc.ai_generated && funcs.push({ description: 'Regenerate AI Image', - event: action(() => { - console.log('COOOORDS', this.dataDoc.width as number, this.dataDoc.y as number); - !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this.dataDoc.x as number, (this.dataDoc.y as number) - 10) : SmartDrawHandler.Instance.hideRegenerate(); + event: action(e => { + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(e?.x || 0, e?.y || 0) : SmartDrawHandler.Instance.hideRegenerate(); }), icon: 'pen-to-square', }); diff --git a/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts deleted file mode 100644 index 514e8a94f..000000000 --- a/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { RefObject } from 'react'; -import { bgColor, canvasSize } from '../ImageEditorUtils/imageEditorConstants'; - -export interface APISuccess { - status: 'success'; - urls: string[]; -} - -export interface APIError { - status: 'error'; - message: string; -} - -export class ImageUtility { - /** - * - * @param canvas Canvas to convert - * @returns Blob of canvas - */ - static canvasToBlob = (canvas: HTMLCanvasElement): Promise => - new Promise(resolve => { - canvas.toBlob(blob => { - if (blob) { - resolve(blob); - } - }, 'image/png'); - }); - - // given a square api image, get the cropped img - static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => { - // Create a new canvas element - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - if (ctx) { - // Clear the canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (width < height) { - // horizontal padding, x offset - const xOffset = (canvasSize - width) / 2; - ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); - } else { - // vertical padding, y offset - const yOffset = (canvasSize - height) / 2; - ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); - } - return canvas; - } - return undefined; - }; - - // converts an image to a canvas data url - static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise => - new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - const canvas = this.getCroppedImg(img, width, height); - if (canvas) { - const dataUrl = canvas.toDataURL(); - resolve(dataUrl); - } - }; - img.onerror = error => { - reject(error); - }; - img.src = imageSrc; - }); - - // calls the openai api to get image edits - static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise => { - const apiUrl = 'https://api.openai.com/v1/images/edits'; - const fd = new FormData(); - fd.append('image', imgBlob, 'image.png'); - fd.append('mask', maskBlob, 'mask.png'); - fd.append('prompt', prompt); - fd.append('size', '1024x1024'); - fd.append('n', n ? JSON.stringify(n) : '1'); - fd.append('response_format', 'b64_json'); - - try { - const res = await fetch(apiUrl, { - method: 'POST', - headers: { - Authorization: `Bearer ${process.env.OPENAI_KEY}`, - }, - body: fd, - }); - const data = await res.json(); - console.log(data.data); - return { - status: 'success', - urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`), - }; - } catch (err) { - console.log(err); - return { status: 'error', message: 'API error.' }; - } - }; - - // mock api call - static mockGetEdit = async (mockSrc: string): Promise => ({ - status: 'success', - urls: [mockSrc, mockSrc, mockSrc], - }); - - // Gets the canvas rendering context of a canvas - static getCanvasContext = (canvasRef: RefObject): CanvasRenderingContext2D | null => { - if (!canvasRef.current) return null; - const ctx = canvasRef.current.getContext('2d'); - if (!ctx) return null; - return ctx; - }; - - // Helper for downloading the canvas (for debugging) - static downloadCanvas = (canvas: HTMLCanvasElement) => { - const url = canvas.toDataURL(); - const downloadLink = document.createElement('a'); - downloadLink.href = url; - downloadLink.download = 'canvas'; - - downloadLink.click(); - downloadLink.remove(); - }; - - // Download the canvas (for debugging) - static downloadImageCanvas = (imgUrl: string) => { - const img = new Image(); - img.src = imgUrl; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - ctx?.drawImage(img, 0, 0, canvasSize, canvasSize); - - this.downloadCanvas(canvas); - }; - }; - - // Clears the canvas - static clearCanvas = (canvasRef: React.RefObject) => { - const ctx = this.getCanvasContext(canvasRef); - if (!ctx || !canvasRef.current) return; - ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); - }; - - // Draws the image to the current canvas - static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject, width: number, height: number) => { - const drawImg = (htmlImg: HTMLImageElement) => { - const ctx = this.getCanvasContext(canvasRef); - if (!ctx) return; - ctx.globalCompositeOperation = 'source-over'; - ctx.clearRect(0, 0, width, height); - ctx.drawImage(htmlImg, 0, 0, width, height); - }; - - if (img.complete) { - drawImg(img); - } else { - img.onload = () => { - drawImg(img); - }; - } - }; - - // Gets the image mask for the openai endpoint - static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => { - const canvas = document.createElement('canvas'); - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - if (!ctx) return undefined; - ctx?.clearRect(0, 0, canvasSize, canvasSize); - ctx.drawImage(paddedCanvas, 0, 0); - - // extract and set padding data - if (srcCanvas.height > srcCanvas.width) { - // horizontal padding, x offset - const xOffset = (canvasSize - srcCanvas.width) / 2; - ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height); - ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height); - } else { - // vertical padding, y offset - const yOffset = (canvasSize - srcCanvas.height) / 2; - ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height); - ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height); - } - return canvas; - }; - - // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) - static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => { - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const { data } = imageData; - for (let i = 0; i < canvas.height; i++) { - for (let j = 0; j < xOffset; j++) { - const targetIdx = 4 * (i * canvas.width + j); - const sourceI = i; - const sourceJ = xOffset + (xOffset - j); - const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); - data[targetIdx] = data[sourceIdx]; - data[targetIdx + 1] = data[sourceIdx + 1]; - data[targetIdx + 2] = data[sourceIdx + 2]; - } - } - for (let i = 0; i < canvas.height; i++) { - for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) { - const targetIdx = 4 * (i * canvas.width + j); - const sourceI = i; - const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j)); - const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); - data[targetIdx] = data[sourceIdx]; - data[targetIdx + 1] = data[sourceIdx + 1]; - data[targetIdx + 2] = data[sourceIdx + 2]; - } - } - ctx.putImageData(imageData, 0, 0); - }; - - // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) - static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => { - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const { data } = imageData; - for (let j = 0; j < canvas.width; j++) { - for (let i = 0; i < yOffset; i++) { - const targetIdx = 4 * (i * canvas.width + j); - const sourceJ = j; - const sourceI = yOffset + (yOffset - i); - const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); - data[targetIdx] = data[sourceIdx]; - data[targetIdx + 1] = data[sourceIdx + 1]; - data[targetIdx + 2] = data[sourceIdx + 2]; - } - } - for (let j = 0; j < canvas.width; j++) { - for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) { - const targetIdx = 4 * (i * canvas.width + j); - const sourceJ = j; - const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i)); - const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); - data[targetIdx] = data[sourceIdx]; - data[targetIdx + 1] = data[sourceIdx + 1]; - data[targetIdx + 2] = data[sourceIdx + 2]; - } - } - ctx.putImageData(imageData, 0, 0); - }; - - // Gets the unaltered (besides filling in padding) version of the image for the api call - static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => { - const canvas = document.createElement('canvas'); - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - if (!ctx) return undefined; - // fix scaling - const scale = Math.min(canvasSize / img.width, canvasSize / img.height); - const width = Math.floor(img.width * scale); - const height = Math.floor(img.height * scale); - ctx?.clearRect(0, 0, canvasSize, canvasSize); - ctx.fillStyle = bgColor; - ctx.fillRect(0, 0, canvasSize, canvasSize); - - // extract and set padding data - if (img.naturalHeight > img.naturalWidth) { - // horizontal padding, x offset - const xOffset = Math.floor((canvasSize - width) / 2); - ctx.drawImage(img, xOffset, 0, width, height); - - // draw reflected image padding - this.drawHorizontalReflection(ctx, canvas, xOffset); - } else { - // vertical padding, y offset - const yOffset = Math.floor((canvasSize - height) / 2); - ctx.drawImage(img, 0, yOffset, width, height); - - // draw reflected image padding - this.drawVerticalReflection(ctx, canvas, yOffset); - } - return canvas; - }; - - /** - * Converts a url to base64 (tainted canvas workaround) - */ - static urlToBase64 = async (imageUrl: string): Promise => { - try { - const res = await fetch(imageUrl); - const blob = await res.blob(); - - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - const base64Data = reader.result?.toString().split(',')[1]; - if (base64Data) { - resolve(base64Data); - } else { - reject(new Error('Failed to convert.')); - } - }; - reader.onerror = () => { - reject(new Error('Error reading image data')); - }; - reader.readAsDataURL(blob); - }); - } catch (err) { - console.error(err); - } - return undefined; - }; -} diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index 1a470f995..7a95e27c2 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -1,5 +1,6 @@ import { imageUrlToBase64 } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; import { ImageCast } from '../../../fields/Types'; import { Upload } from '../../../server/SharedMediaTypes'; import { gptDescribeImage } from '../../apis/gpt/GPT'; @@ -7,19 +8,32 @@ import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { OpenWhere } from '../nodes/OpenWhere'; +import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants'; export class DrawingFillHandler { static drawingToImage = (drawing: Doc, strength: number, prompt: string) => DocumentView.GetDocImage(drawing)?.then(imageField => { if (imageField) { + const aspectRatio = (drawing.width as number) / (drawing.height as number); + let dims: { width: number; height: number }; + if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) { + dims = FireflyDimensionsMap[FireflyImageDimensions.Widescreen]; + } else if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Landscape]) { + dims = FireflyDimensionsMap[FireflyImageDimensions.Landscape]; + } else if (aspectRatio < AspectRatioLimits[FireflyImageDimensions.Portrait]) { + dims = FireflyDimensionsMap[FireflyImageDimensions.Portrait]; + } else { + dims = FireflyDimensionsMap[FireflyImageDimensions.Square]; + } const { href } = ImageCast(imageField).url; const hrefParts = href.split('.'); const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; imageUrlToBase64(structureUrl) .then((hrefBase64: string) => gptDescribeImage(hrefBase64)) .then((prompt: string) => { - Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => - DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {}), OpenWhere.addRight)) // prettier-ignore + Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl, strength }).then((info: Upload.ImageInformation) => + DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight) + ); // prettier-ignore }); } return false; diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts new file mode 100644 index 000000000..f51305fba --- /dev/null +++ b/src/client/views/smartdraw/FireflyConstants.ts @@ -0,0 +1,20 @@ +export enum FireflyImageDimensions { + Square = 'square', + Landscape = 'landscape', + Portrait = 'portrait', + Widescreen = 'widescreen', +} + +export const FireflyDimensionsMap = { + square: { width: 2048, height: 2048 }, + landscape: { width: 2304, height: 1792 }, + portrait: { width: 1792, height: 2304 }, + widescreen: { width: 2688, height: 1536 }, +}; + +export const AspectRatioLimits = { + square: 1, + landscape: 1.167, + portrait: 0.875, + widescreen: 1.472, +}; diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss index 4b21c92a5..cca7d77c7 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.scss +++ b/src/client/views/smartdraw/SmartDrawHandler.scss @@ -1,6 +1,5 @@ .smart-draw-handler { position: absolute; - width: 265px; .smart-draw-main { display: flex; @@ -16,9 +15,11 @@ display: flex; flex-direction: row; justify-content: center; + margin-top: 3px; } .smartdraw-options-container { + width: 265px; padding: 5px; font-weight: bolder; text-align: center; diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 8cff2174f..6c9470480 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -25,6 +25,7 @@ import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillCol import './SmartDrawHandler.scss'; import { Networking } from '../../Network'; import { OpenWhere } from '../nodes/OpenWhere'; +import { FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants'; export interface DrawingOptions { text: string; @@ -35,13 +36,6 @@ export interface DrawingOptions { y: number; } -enum FireflyImageDimensions { - Square = 'square', - Landscape = 'landscape', - Portrait = 'portrait', - Widescreen = 'widescreen', -} - /** * 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 @@ -274,30 +268,12 @@ export class SmartDrawHandler extends ObservableReactComponent { */ createImageWithFirefly = (input: string, seed?: number) => { this._lastInput.text = input; - let width = 0; - let height = 0; - switch (this._imgDims) { - case FireflyImageDimensions.Square: - width = 2048; - height = 2048; - break; - case FireflyImageDimensions.Landscape: - width = 2304; - height = 1792; - case FireflyImageDimensions.Portrait: - width = 1792; - height = 2304; - break; - case FireflyImageDimensions.Widescreen: - width = 2688; - height = 1536; - break; - } - return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: width, height: height, seed: seed }).then(img => { + const dims = FireflyDimensionsMap[this._imgDims]; + return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => { const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { title: input.match(/^(.*?)~~~.*$/)?.[1] || input, - nativeWidth: width, - nativeHeight: height, + nativeWidth: dims.width, + nativeHeight: dims.height, ai_generated: true, firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1], firefly_prompt: input, diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index a41492745..4c4aac5e0 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -20,7 +20,7 @@ export default class FireflyManager extends ApiManager { return undefined; }); - generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', structureUrl: string, strength: number) => + generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50) => this.getBearerToken().then(response => response?.json().then((data: { access_token: string }) => fetch('https://firefly-api.adobe.io/v3/images/generate', { @@ -32,7 +32,8 @@ export default class FireflyManager extends ApiManager { ['Authorization', `Bearer ${data.access_token}`], ], body: JSON.stringify({ - prompt, + prompt: prompt, + size: { width: width, height: height }, structure: !structureUrl ? undefined : { @@ -226,7 +227,7 @@ export default class FireflyManager extends ApiManager { subscription: '/queryFireflyImageFromStructure', secureHandler: async ({ req, res }) => this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl => - this.generateImageFromStructure(req.body.prompt, uploadUrl, req.body.strength).then(fire => + this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength).then(fire => DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => { if (info instanceof Error) _invalid(res, info.message); else _success(res, info); -- cgit v1.2.3-70-g09d2 From 215ad40efa2e343e290d18bffbc55884829f1a0d Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 4 Mar 2025 00:52:53 -0500 Subject: 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) --- src/client/documents/DocUtils.ts | 57 +++- src/client/util/bezierFit.ts | 302 +++++++++++++++++---- src/client/views/collections/CollectionSubView.tsx | 17 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 36 +-- src/client/views/pdf/AnchorMenu.tsx | 2 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 72 +++-- 6 files changed, 363 insertions(+), 123 deletions(-) (limited to 'src/client/util/bezierFit.ts') 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((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(/]*>([\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, 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, 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() { 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 { 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 { - 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 { - 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 { 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 { * 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 { 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 { /** * 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 { * 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 { 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 { /** * 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 { 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 { */ parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => { const svg = res.match(/]*>([\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 { }, }} checked={this._generateImage} - onChange={() => this._canInteract && (this._generateImage = !this._generateImage)} + onChange={action(() => this._canInteract && (this._generateImage = !this._generateImage))} /> @@ -566,7 +584,7 @@ export class SmartDrawHandler extends ObservableReactComponent { icon={this._isLoading ? : } iconPlacement="right" color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> {this._showOptions && ( @@ -598,7 +616,7 @@ export class SmartDrawHandler extends ObservableReactComponent { icon={this._isLoading && this._regenInput !== '' ? : } iconPlacement="right" color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> ); @@ -644,7 +662,7 @@ export class SmartDrawHandler extends ObservableReactComponent { tooltip="Regenerate" icon={this._isLoading && this._regenInput === '' ? : } color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> } color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> {this._showEditBox ? this.renderRegenerateEditBox() : null} -- cgit v1.2.3-70-g09d2 From 5a9b1a453426df073b0ca43b54331d795ffea365 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 4 Mar 2025 13:46:48 -0500 Subject: fixed svg conversion to bezier to handle relative points and dataformats without spaces. --- src/client/util/bezierFit.ts | 214 +++++++++++++++---------------------------- 1 file changed, 76 insertions(+), 138 deletions(-) (limited to 'src/client/util/bezierFit.ts') diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 65bd44bf9..0399fe1d5 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -1,6 +1,7 @@ /* eslint-disable no-use-before-define */ /* eslint-disable no-param-reassign */ import { Point } from '../../pen-gestures/ndollar'; +import { numberRange } from '../../Utils'; export enum SVGType { Rect = 'rect', @@ -623,24 +624,20 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) { return [...firstEnd, ...points, ...lastEnd]; } -function convertToAbsolute(pathData: string): string { +function convertRelativePathCmdsToAbsolute(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 absoluteCommands = commands?.map(command => { const values = command .slice(1) .trim() .split(/[\s,]+/) .map(v => +v); - switch (type) { + switch (command[0]) { case 'M': currentX = values[0]; currentY = values[1]; @@ -654,13 +651,19 @@ function convertToAbsolute(pathData: string): string { 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}`; + currentX = values[values.length - 2]; + currentY = values[values.length - 1]; + return `L${values.join(',')}`; + case 'l': { + let str = ''; + for (let i = 0; i < values.length; i += 2) { + str += (i === 0 ? 'L':',') + (values[i] + currentX) + + ',' + (values[i + 1] + currentY); // prettier-ignore + currentX += values[i]; + currentY += values[i + 1]; + } + return str; + } case 'H': currentX = values[0]; return `H${currentX}`; @@ -674,8 +677,8 @@ function convertToAbsolute(pathData: string): string { currentY += values[0]; return `V${currentY}`; case 'C': - currentX = values[4]; - currentY = values[5]; + currentX = values[values.length - 2]; + currentY = values[values.length - 1]; return `C${values.join(',')}`; case 'c': { let str = ''; @@ -698,8 +701,8 @@ function convertToAbsolute(pathData: string): string { case 's': return `S${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`; case 'Q': - currentX = values[2]; - currentY = values[3]; + currentX = values[values.length - 2]; + currentY = values[values.length - 1]; return `Q${values.join(',')}`; case 'q': { let str = ''; @@ -737,7 +740,7 @@ function convertToAbsolute(pathData: string): string { } }); - return absoluteCommands.join(' '); + return absoluteCommands?.join(' ') ?? pathData; } export function SVGToBezier(name: SVGType, attributes: Record, last: { X: number; Y: number }): Point[] { @@ -805,138 +808,73 @@ export function SVGToBezier(name: SVGType, attributes: Record, l ]; } case 'path': { + const cmds = new Map([ + ['A', 7], + ['C', 6], + ['Q', 4], + ['L', 2], + ['V', 1], + ['H', 1], + ['Z', 0], + ['M', 2], + ]); + const cmdReg = (letter: string) => `${letter}?${numberRange(cmds.get(letter)??0).map(() => '[, ]?(-?\\d*\\.?\\d*)').join('')}`; // prettier-ignore + const pathdata = convertRelativePathCmdsToAbsolute( + attributes.d + .replace(/([0-9])-/g, '$1,-') // numbers are smooshed together - put a ',' between number-number => number,-number + .replace(/([.][0-9]+)(?=\.)/g, '$1,') // numbers are smooshed together - put a ',' between .number.number => .number,.number + .trim() + ); + const move = pathdata.match(cmdReg('M')); + const start = move?.slice(1).map(v => +v) ?? [last.X, last.Y]; const coordList: Point[] = []; - 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); - } + for (let prev = coordList.lastElement() ?? { X: start[0], Y: start[1] }, + pathcmd = pathdata.slice(move?.[0].length ?? 0).trim(), + m = move, + lastCmd = ''; + pathcmd; + pathcmd = pathcmd.slice(m?.[0].length ?? 1).trim(), + prev = coordList.lastElement() + ) { + lastCmd = Array.from(cmds.keys()).includes(pathcmd[0]) ? pathcmd[0] : lastCmd; // command character is first, otherwise we're continuing coordinates for the last command + m = pathcmd.match(new RegExp(cmdReg(lastCmd)))!; // matches command + number parameters specific to command + switch (m ? lastCmd : 'error') { + case 'Q': // convert quadratic to Bezier + ((Q) => coordList.push( + prev, + { X: prev.X + (2 / 3) * (Q[0] - prev.X), Y: prev.Y + (2 / 3) * (Q[1] - prev.Y) }, + { X: Q[2] + (2 / 3) * (Q[0] - Q[2]), Y: Q[3] + (2 / 3) * (Q[1] - Q[3]) }, + { X: Q[2], Y: Q[3] } + ))([+m[1], +m[2], +m[3], +m[4]]); + break; case 'C': // bezier curve + coordList.push(prev, { X: +m[1], Y: +m[2] }, { X: +m[3], Y: +m[4] }, { X: +m[5], Y: +m[6] }); + break; case 'L': // convert line to bezier + coordList.push(prev, prev, { X: +m[1], Y: +m[2] }, { X: +m[1], Y: +m[2] }); + break; case 'H': // convert horiz line to bezier + coordList.push(prev, prev, { X: +m[1], Y: prev.Y }, { X: +m[1], Y: prev.Y }); + break; case 'V': // convert vert line to bezier + coordList.push(prev, prev, { X: prev.X, Y: +m[1] }, { X: prev.X, Y: +m[1] }); + break; case 'A': // convert arc to bezier + console.log('SKIPPING arc - conversion to bezier not implemented'); + break; case 'Z': 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(); + // eslint-disable-next-line no-debugger debugger; - } - first = false; - } + } // prettier-ignore + } // prettier-ignore return coordList; } case 'polygon': { - const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g)); - let list: Point[] = []; + const coords = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g)); + const list: Point[] = []; coords.forEach(coord => { 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); - return list; + return list.concat(list.splice(0, 2)); // repeat start point to close } default: return []; -- cgit v1.2.3-70-g09d2