aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts3
-rw-r--r--src/client/util/CurrentUserUtils.ts2
-rw-r--r--src/client/util/bezierFit.ts116
-rw-r--r--src/client/views/GestureOverlay.tsx28
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx51
7 files changed, 178 insertions, 32 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 0c993680e..06c562562 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,7 +58,8 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
model: 'gpt-4o',
maxTokens: 1024,
temp: 0.5,
- prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Respond only with the coordinates',
+ prompt: 'Given an item, generate a detailed line drawing representation of it. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
+ // prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Format your response like this: M (100,30) C (75,10) (25,10) (50,50) C (25,75) (10,125) (50,150) C (25,75) (10,125) (50,150) and give no additional text. If a disconnected stroke is required, repeat that pattern with a new M marker',
},
};
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b6a7cacba..141695d86 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -743,7 +743,7 @@ pie title Minerals in my tap water
{ title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
{ title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
{ title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
- { title: "Labels", toolTip: "Lab els", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
+ { title: "Labels", toolTip: "Labels", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
{ title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
{ title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
{ title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: "smartdraw", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index fbc2bb7cd..f5696afaf 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -2,8 +2,18 @@
/* eslint-disable prefer-destructuring */
/* eslint-disable no-param-reassign */
/* eslint-disable camelcase */
+import e from 'cors';
import { Point } from '../../pen-gestures/ndollar';
+export enum SVGType {
+ Rect = 'rect',
+ Path = 'path',
+ Circle = 'circle',
+ Ellipse = 'ellipse',
+ Line = 'line',
+ Polygon = 'polygon',
+}
+
class SmartRect {
minx: number = 0;
miny: number = 0;
@@ -616,6 +626,112 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
return [...firstEnd, ...points, ...lastEnd];
}
+export function SVGToBezier(name: SVGType, attributes: any): Point[] {
+ console.log('in svg to bezier', name, attributes);
+ switch (name) {
+ case 'line':
+ const x1 = parseInt(attributes.x1);
+ const x2 = parseInt(attributes.x2);
+ const y1 = parseInt(attributes.y1);
+ const y2 = parseInt(attributes.y2);
+ return [
+ { X: x1, Y: y1 },
+ { X: x1, Y: y1 },
+ { X: x2, Y: y2 },
+ { X: x2, Y: y2 },
+ ];
+ 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);
+ return [
+ { X: centerX, Y: centerY + radiusY },
+ { X: centerX + c * radiusX, Y: centerY + radiusY },
+ { X: centerX + radiusX, Y: centerY + c * radiusY },
+ { X: centerX + radiusX, Y: centerY },
+ { X: centerX + radiusX, Y: centerY },
+ { X: centerX + radiusX, Y: centerY - c * radiusY },
+ { X: centerX + c * radiusX, Y: centerY - radiusY },
+ { X: centerX, Y: centerY - radiusY },
+ { X: centerX, Y: centerY - radiusY },
+ { X: centerX - c * radiusX, Y: centerY - radiusY },
+ { X: centerX - radiusX, Y: centerY - c * radiusY },
+ { X: centerX - radiusX, Y: centerY },
+ { X: centerX - radiusX, Y: centerY },
+ { X: centerX - radiusX, Y: centerY + c * radiusY },
+ { X: centerX - c * radiusX, Y: centerY + radiusY },
+ { X: centerX, Y: centerY + radiusY },
+ ];
+ case 'rect':
+ const x = parseInt(attributes.x);
+ const y = parseInt(attributes.y);
+ const width = parseInt(attributes.width);
+ const height = parseInt(attributes.height);
+ return [
+ { X: x, Y: y },
+ { X: x, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y + height },
+ { X: x + width, Y: y + height },
+ { X: x + width, Y: y + height },
+ { X: x + width, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y },
+ { X: x, Y: y },
+ ];
+ case 'path':
+ const coordList: Point[] = [];
+ const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/);
+ coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ const matches: RegExpMatchArray[] = Array.from(
+ attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
+ );
+ let lastPt: Point;
+ 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 || { X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ 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]) };
+ }
+ });
+ 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]) });
+ } else {
+ coordList.pop();
+ }
+ return coordList;
+ case 'polygon':
+ default:
+ return [];
+ }
+}
+
/*
static double GetTValueFromSValue (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) {
double dist = 0;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 2f26bdaef..dc6edf81d 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -242,15 +242,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this._points.length = 0;
switch (shape) {
case Gestures.Rectangle:
- this._points.push({ X: left, Y: top });
- this._points.push({ X: left, Y: top });
- this._points.push({ X: right, Y: top });
- this._points.push({ X: right, Y: top });
+ this._points.push({ X: left, Y: top }); // curr pt
+ this._points.push({ X: left, Y: top }); // curr first ctrl pt
+ this._points.push({ X: right, Y: top }); // next ctrl pt
+ this._points.push({ X: right, Y: top }); // next pt
- this._points.push({ X: right, Y: top });
- this._points.push({ X: right, Y: top });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
+ this._points.push({ X: right, Y: top }); // next pt
+ this._points.push({ X: right, Y: top }); // next first ctrl pt
+ this._points.push({ X: right, Y: bottom }); // next next ctrl pt
+ this._points.push({ X: right, Y: bottom }); // next next pt
this._points.push({ X: right, Y: bottom });
this._points.push({ X: right, Y: bottom });
@@ -293,13 +293,13 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
// Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
- this._points.push({ X: centerX, Y: centerY + radius });
- this._points.push({ X: centerX + c * radius, Y: centerY + radius });
- this._points.push({ X: centerX + radius, Y: centerY + c * radius });
- this._points.push({ X: centerX + radius, Y: centerY });
+ this._points.push({ X: centerX, Y: centerY + radius }); // curr pt
+ this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+ this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+ this._points.push({ X: centerX + radius, Y: centerY }); // next pt
- this._points.push({ X: centerX + radius, Y: centerY });
- this._points.push({ X: centerX + radius, Y: centerY - c * radius });
+ this._points.push({ X: centerX + radius, Y: centerY }); // next pt
+ this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
this._points.push({ X: centerX + c * radius, Y: centerY - radius });
this._points.push({ X: centerX, Y: centerY - radius });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 44e00396e..cbc337860 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,6 +76,7 @@ import { PresBox } from './nodes/trails';
import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
+import { SmartDrawHandler } from './collections/collectionFreeForm/SmartDrawHandler';
const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
const _global = (window /* browser */ || global) /* node */ as any;
@@ -1091,6 +1092,7 @@ export class MainView extends ObservableReactComponent<{}> {
<TaskCompletionBox />
<ContextMenu />
<ImageLabelHandler />
+ <SmartDrawHandler />
<AnchorMenu />
<MapAnchorMenu />
<DirectionsAnchorMenu />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a27ac2a0c..93b63ac4c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1267,17 +1267,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStrokes);
};
- @action
+ @undoBatch
createInkStrokes = (strokeList: InkData[], alpha?: number) => {
console.log(strokeList.length);
strokeList.forEach(inkData => {
// const points: InkData = FitCurve(inkData, 20) as InkData;
- const allPts = GenerateControlPoints(inkData, alpha);
- const bounds = InkField.getBounds(allPts);
+ // const allPts = GenerateControlPoints(inkData, alpha);
+ const bounds = InkField.getBounds(inkData);
const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
const inkDoc = Docs.Create.InkDocument(
- allPts,
+ inkData,
{ title: 'stroke',
x: B.x - inkWidth / 2,
y: B.y - inkWidth / 2,
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
index 4c2e78e31..956a8d7e9 100644
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -12,6 +12,8 @@ import './ImageLabelHandler.scss';
import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
import { InkData } from '../../../../fields/InkField';
import { ButtonType } from '../../nodes/FontIconBox/FontIconBox';
+import { SVGToBezier } from '../../../util/bezierFit';
+const { parse, stringify } = require('svgson');
@observer
export class SmartDrawHandler extends ObservableReactComponent<{}> {
@@ -69,19 +71,44 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
return;
}
console.log('GPT response:', res);
- const simplifiedRes: string = res.replace(/[^\d\[\],]/g, '');
- try {
- const parsedPts = JSON.parse(simplifiedRes);
- if (parsedPts[0][0][0]) {
- const controlPts = (parsedPts as [number, number][][]).map((stroke: [number, number][]) => stroke.map(([X, Y]) => ({ X: X + startPoint.X - 100, Y: Y + startPoint.Y - 100 })));
- this._addToDocFunc(controlPts, this._alpha);
- } else {
- const controlPts = (parsedPts as [number, number][]).map(([X, Y]) => ({ X: X + startPoint.X - 100, Y: Y + startPoint.Y - 100 }));
- this._addToDocFunc([controlPts], this._alpha);
- }
- } catch (err) {
- console.error('Error likely from bad GPT output type');
+ const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ console.log('svg', svg);
+ if (svg) {
+ const svgObject = await parse(svg[0]);
+ console.log('svg object', svgObject);
+ const svgStrokes: any = svgObject.children;
+ const beziers: InkData[] = [];
+ svgStrokes.forEach((stroke: any) => {
+ const convertedBezier: InkData = SVGToBezier(stroke.name, stroke.attributes);
+ beziers.push(
+ convertedBezier.map(point => {
+ return { X: point.X + startPoint.X, Y: point.Y + startPoint.Y };
+ })
+ );
+ });
+ this._addToDocFunc(beziers);
}
+
+ // const strokes = res.trim().split(/\s*(?=\s*M)/); // prettier-ignore
+ // const parsedSegments: InkData[] = [];
+ // console.log('strokes', strokes);
+ // strokes.forEach(stroke => {
+ // stroke = stroke.replace(/C\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)/g, (c, p1, p2, p3) => {
+ // return `C (${p1}) (${p2}) (${p3}) (${p3})`;
+ // });
+ // const coordStrings = stroke.match(/(\d+,\d+)/g);
+ // const coords: InkData = [];
+ // if (coordStrings) {
+ // coordStrings.forEach(coord => {
+ // const xy = coord.split(',');
+ // coords.push({ X: parseInt(xy[0]), Y: parseInt(xy[1]) });
+ // });
+ // coords.pop();
+ // parsedSegments.push(coords);
+ // }
+ // console.log('coords', coords);
+ // });
+ // this._addToDocFunc(parsedSegments);
} catch (err) {
console.error('GPT call failed', err);
}