diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/CurrentUserUtils.ts | 33 | ||||
| -rw-r--r-- | src/client/util/bezierFit.ts | 145 |
2 files changed, 177 insertions, 1 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 8ece897f4..98a54d4d0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -162,6 +162,33 @@ export class CurrentUserUtils { const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } + + static setupAnnoPalette(doc: Doc, field="myAnnos") { + const reqdOpts:DocumentOptions = { + title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, hidden: false, + _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true, + _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, + }; + const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; + const savedAnnos = DocCast(doc[field]); + return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.MasonryDocument(items??[], opts), reqdOpts, DocListCast(savedAnnos?.data), reqdScripts); + } + + // static setupNoteTemplates(doc: Doc, field="template_notes") { + // const tempNotes = DocCast(doc[field]); + // const reqdTempOpts:DocumentOptions[] = [ + // { title: "Postit", backgroundColor: "yellow", icon: "sticky-note", _layout_showTitle: "title", layout_borderRounding: "5px"}, + // { title: "Idea", backgroundColor: "pink", icon: "lightbulb" , _layout_showTitle: "title"}, + // { title: "Topic", backgroundColor: "lightblue", icon: "book-open" , _layout_showTitle: "title"}]; + // const reqdNoteList = [...reqdTempOpts.map(opts => { + // const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true}; + // const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(fdoc => fdoc.title === opts.title): undefined; + // return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts)); + // }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; + + // const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; + // // eslint-disable-next-line no-return-assign + // r // setup templates for different document types when they are iconified from Document Decorations static setupDefaultIconTemplates(doc: Doc, field="template_icons") { @@ -414,6 +441,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, + { toolTip: "Tap or drag to create an annotation palette",title: "Annotation Palette", icon: "palette", dragFactory: doc.emptyAnnoPalette as Doc, clickFactory: DocCast(doc.emptyAnnoPalette)}, { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script // { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack />" as any, openFactoryLocation: OpenWhere.overlay}, ].map(tuple => ( @@ -730,6 +758,7 @@ pie title Minerals in my tap water static inkTools():Button[] { return [ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }}, + { title: "Highlight", toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }}, { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"}, subMenu: [ @@ -742,9 +771,10 @@ 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()"}}, ]; } @@ -984,6 +1014,7 @@ pie title Minerals in my tap water this.setupTopbarButtons(doc); this.setupDockedButtons(doc); // the bottom bar of font icons this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left + this.setupAnnoPalette(doc); this.setupDocTemplates(doc); // sets up the template menu of templates // sthis.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption DocUtils.AssignDocField(doc, "globalScriptDatabase", () => Docs.Prototypes.MainScriptDocument(), {}); diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index d6f3f2340..98a6feea0 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; @@ -557,6 +567,12 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y); FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result); } +/** + * Convert polyline coordinates to a (multi) segment bezier curve + * @param d - polyline coordinates + * @param error - how much error to allow in fitting (measured in pixels) + * @returns + */ export function FitCurve(d: Point[], error: number) { const tHat1 = ComputeLeftTangent(d, 0); // Unit tangent vectors at endpoints const tHat2 = ComputeRightTangent(d, d.length - 1); @@ -586,6 +602,135 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) { return { finalCtrls, error }; } +// alpha determines how far away the tangents are, or the "tightness" of the bezier +export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) { + const firstEnd = coordinates.length ? [coordinates[0], coordinates[0]] : []; + const lastEnd = coordinates.length ? [coordinates.lastElement(), coordinates.lastElement()] : []; + const points: Point[] = coordinates.slice(1, coordinates.length - 1).flatMap((pt, index, inkData) => { + const prevPt: Point = index === 0 ? firstEnd[0] : inkData[index - 1]; + const nextPt: Point = index === inkData.length - 1 ? lastEnd[0] : inkData[index + 1]; + if (prevPt.X === nextPt.X) { + const verticalDist = nextPt.Y - prevPt.Y; + return [{ X: pt.X, Y: pt.Y - alpha * verticalDist }, pt, pt, { X: pt.X, Y: pt.Y + alpha * verticalDist }]; + } else if (prevPt.Y === nextPt.Y) { + const horizDist = nextPt.X - prevPt.X; + return [{ X: pt.X - alpha * horizDist, Y: pt.Y }, pt, pt, { X: pt.X + alpha * horizDist, Y: pt.Y }]; + } + // tangent vectors between the adjacent points + const tanX = nextPt.X - prevPt.X; + const tanY = nextPt.Y - prevPt.Y; + const ctrlPt1: Point = { X: pt.X - alpha * tanX, Y: pt.Y - alpha * tanY }; + const ctrlPt2: Point = { X: pt.X + alpha * tanX, Y: pt.Y + alpha * tanY }; + return [ctrlPt1, pt, pt, ctrlPt2]; + }); + return [...firstEnd, ...points, ...lastEnd]; +} + +export function SVGToBezier(name: SVGType, attributes: any): 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); + 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; |
