diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/CurrentUserUtils.ts | 40 | ||||
| -rw-r--r-- | src/client/util/DropConverter.ts | 26 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.tsx | 2 | ||||
| -rw-r--r-- | src/client/util/bezierFit.ts | 160 |
4 files changed, 220 insertions, 8 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 14fb65252..cdf5505f8 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -158,6 +158,26 @@ 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", 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 setupLightboxDrawingPreviews(doc: Doc, field="myLightboxDrawings") { + const reqdOpts:DocumentOptions = { + title: "Preview", _header_height: 0, _layout_fitWidth: true, childLayoutFitWidth: true, + }; + const reqdScripts = {}; + const drawings = DocCast(doc[field]); + return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.CarouselDocument(items??[], opts), reqdOpts, DocListCast(drawings?.data), reqdScripts); + } // setup templates for different document types when they are iconified from Document Decorations static setupDefaultIconTemplates(doc: Doc, field="template_icons") { @@ -365,7 +385,8 @@ pie title Minerals in my tap water {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }}, - {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 300, _height: 300, }}, + {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 300, _height: 300, }}, + {key: "AnnoPalette",creator: Docs.Create.AnnoPaletteDocument, opts: {_width: 300, _height: 300, _dropAction: dropActionType.move }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}}, {key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}}, {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, @@ -392,11 +413,11 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a plotly node", title: "Plotly", icon: "rocket", dragFactory: doc.emptyPlotly as Doc, clickFactory: DocCast(doc.emptyMermaids)}, { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "book", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, - { toolTip: "Tap or drag to create an iamge", title: "Image", icon: "image", dragFactory: doc.emptyImage as Doc, clickFactory: DocCast(doc.emptyImage)}, + { toolTip: "Tap or drag to create an image", title: "Image", icon: "image", dragFactory: doc.emptyImage as Doc, clickFactory: DocCast(doc.emptyImage)}, { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)}, { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)}, - { toolTip: "Tap or drag to create a diagram", title: "Diagram", icon: "tree", dragFactory: doc.emptyDiagram as Doc, clickFactory: DocCast(doc.emptyDiagram)}, + { toolTip: "Tap or drag to create a diagram", title: "Diagram", icon: "tree", dragFactory: doc.emptyDiagram as Doc, clickFactory: DocCast(doc.emptyDiagram)}, { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay}, { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)}, { toolTip: "Tap or drag to create a chat assistant", title: "Assistant Chat", icon: "book",dragFactory: doc.emptyChat as Doc, clickFactory: DocCast(doc.emptyChat)}, @@ -405,9 +426,10 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, - { 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 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 unknown as Doc, 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 => ( @@ -738,6 +760,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: [ @@ -750,9 +773,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()"}}, ]; } @@ -992,6 +1016,8 @@ 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.setupLightboxDrawingPreviews(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/DropConverter.ts b/src/client/util/DropConverter.ts index eb2011b77..57c79cff3 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -87,6 +87,32 @@ export function makeUserTemplateButton(doc: Doc) { dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); return dbox; } + +/** + * Similar to makeUserTemplateButton, but rather than creating a draggable button for the template, it takes in + * an ImageField that will display. + */ +export function makeUserTemplateImage(doc: Doc, image: ImageField) { + const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + if (layoutDoc.type !== DocumentType.FONTICON) { + !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); + } + layoutDoc.isTemplateDoc = true; + const dbox = Docs.Create.ImageDocument(image, { + _nativeWidth: 100, + _nativeHeight: 100, + _width: 100, + _height: 100, + title: StrCast(layoutDoc.title), + isSystem: false, + }); + dbox.title = ComputedField.MakeFunction('this.dragFactory.title'); + dbox.dragFactory = layoutDoc; + dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; + dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); + return dbox; +} + export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.forEach((doc, i) => { let dbox = doc; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index fde8869e3..9200d68db 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -85,7 +85,7 @@ export class SettingsManager extends React.Component<object> { if (this._playgroundMode) { DocServer.Control.makeReadOnly(); addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); + } else if (ClientUtils.CurrentUserEmail() !== 'guest') DocServer.Control.makeEditable(); }), 'set playgorund mode' ); diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index d6f3f2340..4c7f4a0ba 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -4,6 +4,15 @@ /* eslint-disable camelcase */ 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 +566,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 +601,151 @@ 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 = { X: 0, Y: 0 }; + 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': { + 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]) }); + }); + const firstPts = list.splice(0, 2); + list = list.concat(firstPts); + return list; + } + default: + return []; + } +} + /* static double GetTValueFromSValue (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) { double dist = 0; |
