@svgedit/svgcanvas 7.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/path.js ADDED
@@ -0,0 +1,781 @@
1
+ /**
2
+ * Path functionality.
3
+ * @module path
4
+ * @license MIT
5
+ *
6
+ * @copyright 2011 Alexis Deveria, 2011 Jeff Schiller
7
+ */
8
+
9
+ import { shortFloat } from '../../src/common/units.js'
10
+ import { transformPoint } from './math.js'
11
+ import {
12
+ getRotationAngle, getBBox,
13
+ getRefElem, findDefs,
14
+ getBBox as utilsGetBBox
15
+ } from './utilities.js'
16
+ import {
17
+ init as pathMethodInit, ptObjToArrMethod, getGripPtMethod,
18
+ getPointFromGripMethod, addPointGripMethod, getGripContainerMethod, addCtrlGripMethod,
19
+ getCtrlLineMethod, getPointGripMethod, getControlPointsMethod, replacePathSegMethod,
20
+ getSegSelectorMethod, Path
21
+ } from './path-method.js'
22
+ import {
23
+ init as pathActionsInit, pathActionsMethod
24
+ } from './path-actions.js'
25
+
26
+ const segData = {
27
+ 2: ['x', 'y'], // PATHSEG_MOVETO_ABS
28
+ 4: ['x', 'y'], // PATHSEG_LINETO_ABS
29
+ 6: ['x', 'y', 'x1', 'y1', 'x2', 'y2'], // PATHSEG_CURVETO_CUBIC_ABS
30
+ 8: ['x', 'y', 'x1', 'y1'], // PATHSEG_CURVETO_QUADRATIC_ABS
31
+ 10: ['x', 'y', 'r1', 'r2', 'angle', 'largeArcFlag', 'sweepFlag'], // PATHSEG_ARC_ABS
32
+ 12: ['x'], // PATHSEG_LINETO_HORIZONTAL_ABS
33
+ 14: ['y'], // PATHSEG_LINETO_VERTICAL_ABS
34
+ 16: ['x', 'y', 'x2', 'y2'], // PATHSEG_CURVETO_CUBIC_SMOOTH_ABS
35
+ 18: ['x', 'y'] // PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS
36
+ }
37
+
38
+ let svgCanvas
39
+ /**
40
+ * @tutorial LocaleDocs
41
+ * @typedef {module:locale.LocaleStrings|PlainObject} module:path.uiStrings
42
+ * @property {PlainObject<string, string>} ui
43
+ */
44
+
45
+ const uiStrings = {}
46
+ /**
47
+ * @function module:path.setUiStrings
48
+ * @param {module:path.uiStrings} strs
49
+ * @returns {void}
50
+ */
51
+ export const setUiStrings = (strs) => {
52
+ Object.assign(uiStrings, strs.ui)
53
+ }
54
+
55
+ let pathFuncs = []
56
+
57
+ let linkControlPts = true
58
+
59
+ // Stores references to paths via IDs.
60
+ // TODO: Make this cross-document happy.
61
+ let pathData = {}
62
+
63
+ /**
64
+ * @function module:path.setLinkControlPoints
65
+ * @param {boolean} lcp
66
+ * @returns {void}
67
+ */
68
+ export const setLinkControlPoints = (lcp) => {
69
+ linkControlPts = lcp
70
+ }
71
+
72
+ /**
73
+ * @name module:path.path
74
+ * @type {null|module:path.Path}
75
+ * @memberof module:path
76
+ */
77
+ export let path = null
78
+
79
+ /**
80
+ * @external MouseEvent
81
+ */
82
+
83
+ /**
84
+ * Object with the following keys/values.
85
+ * @typedef {PlainObject} module:path.SVGElementJSON
86
+ * @property {string} element - Tag name of the SVG element to create
87
+ * @property {PlainObject<string, string>} attr - Has key-value attributes to assign to the new element.
88
+ * An `id` should be set so that {@link module:utilities.EditorContext#addSVGElementsFromJson} can later re-identify the element for modification or replacement.
89
+ * @property {boolean} [curStyles=false] - Indicates whether current style attributes should be applied first
90
+ * @property {module:path.SVGElementJSON[]} [children] - Data objects to be added recursively as children
91
+ * @property {string} [namespace="http://www.w3.org/2000/svg"] - Indicate a (non-SVG) namespace
92
+ */
93
+ /**
94
+ * @interface module:path.EditorContext
95
+ * @property {module:select.SelectorManager} selectorManager
96
+ * @property {module:svgcanvas.SvgCanvas} canvas
97
+ */
98
+ /**
99
+ * @function module:path.EditorContext#call
100
+ * @param {"selected"|"changed"} ev - String with the event name
101
+ * @param {module:svgcanvas.SvgCanvas#event:selected|module:svgcanvas.SvgCanvas#event:changed} arg - Argument to pass through to the callback function.
102
+ * If the event is "changed", an array of `Element`s is passed; if "selected", a single-item array of `Element` is passed.
103
+ * @returns {void}
104
+ */
105
+ /**
106
+ * Note: This doesn't round to an integer necessarily.
107
+ * @function module:path.EditorContext#round
108
+ * @param {Float} val
109
+ * @returns {Float} Rounded value to nearest value based on `zoom`
110
+ */
111
+ /**
112
+ * @function module:path.EditorContext#clearSelection
113
+ * @param {boolean} [noCall] - When `true`, does not call the "selected" handler
114
+ * @returns {void}
115
+ */
116
+ /**
117
+ * @function module:path.EditorContext#addToSelection
118
+ * @param {Element[]} elemsToAdd - An array of DOM elements to add to the selection
119
+ * @param {boolean} showGrips - Indicates whether the resize grips should be shown
120
+ * @returns {void}
121
+ */
122
+ /**
123
+ * @function module:path.EditorContext#addCommandToHistory
124
+ * @param {Command} cmd
125
+ * @returns {void}
126
+ */
127
+ /**
128
+ * @function module:path.EditorContext#remapElement
129
+ * @param {Element} selected - DOM element to be changed
130
+ * @param {PlainObject<string, string>} changes - Object with changes to be remapped
131
+ * @param {SVGMatrix} m - Matrix object to use for remapping coordinates
132
+ * @returns {void}
133
+ */
134
+ /**
135
+ * @function module:path.EditorContext#addSVGElementsFromJson
136
+ * @param {module:path.SVGElementJSON} data
137
+ * @returns {Element} The new element
138
+ */
139
+ /**
140
+ * @function module:path.EditorContext#getGridSnapping
141
+ * @returns {boolean}
142
+ */
143
+ /**
144
+ * @function module:path.EditorContext#getOpacity
145
+ * @returns {Float}
146
+ */
147
+ /**
148
+ * @function module:path.EditorContext#getSelectedElements
149
+ * @returns {Element[]} the array with selected DOM elements
150
+ */
151
+ /**
152
+ * @function module:path.EditorContext#getContainer
153
+ * @returns {Element}
154
+ */
155
+ /**
156
+ * @function module:path.EditorContext#setStarted
157
+ * @param {boolean} s
158
+ * @returns {void}
159
+ */
160
+ /**
161
+ * @function module:path.EditorContext#getRubberBox
162
+ * @returns {SVGRectElement}
163
+ */
164
+ /**
165
+ * @function module:path.EditorContext#setRubberBox
166
+ * @param {SVGRectElement} rb
167
+ * @returns {SVGRectElement} Same as parameter passed in
168
+ */
169
+ /**
170
+ * @function module:path.EditorContext#addPtsToSelection
171
+ * @param {PlainObject} cfg
172
+ * @param {boolean} cfg.closedSubpath
173
+ * @param {SVGCircleElement[]} cfg.grips
174
+ * @returns {void}
175
+ */
176
+ /**
177
+ * @function module:path.EditorContext#endChanges
178
+ * @param {PlainObject} cfg
179
+ * @param {string} cfg.cmd
180
+ * @param {Element} cfg.elem
181
+ * @returns {void}
182
+ */
183
+ /**
184
+ * @function module:path.EditorContext#getZoom
185
+ * @returns {Float} The current zoom level
186
+ */
187
+ /**
188
+ * Returns the last created DOM element ID string.
189
+ * @function module:path.EditorContext#getId
190
+ * @returns {string}
191
+ */
192
+ /**
193
+ * Creates and returns a unique ID string for a DOM element.
194
+ * @function module:path.EditorContext#getNextId
195
+ * @returns {string}
196
+ */
197
+ /**
198
+ * Gets the desired element from a mouse event.
199
+ * @function module:path.EditorContext#getMouseTarget
200
+ * @param {external:MouseEvent} evt - Event object from the mouse event
201
+ * @returns {Element} DOM element we want
202
+ */
203
+ /**
204
+ * @function module:path.EditorContext#getCurrentMode
205
+ * @returns {string}
206
+ */
207
+ /**
208
+ * @function module:path.EditorContext#setCurrentMode
209
+ * @param {string} cm The mode
210
+ * @returns {string} The same mode as passed in
211
+ */
212
+ /**
213
+ * @function module:path.EditorContext#setDrawnPath
214
+ * @param {SVGPathElement|null} dp
215
+ * @returns {SVGPathElement|null} The same value as passed in
216
+ */
217
+ /**
218
+ * @function module:path.EditorContext#getSvgRoot
219
+ * @returns {SVGSVGElement}
220
+ */
221
+
222
+ /**
223
+ * @function module:path.init
224
+ * @param {module:path.EditorContext} editorContext
225
+ * @returns {void}
226
+ */
227
+ export const init = (canvas) => {
228
+ svgCanvas = canvas
229
+ svgCanvas.replacePathSeg = replacePathSegMethod
230
+ svgCanvas.addPointGrip = addPointGripMethod
231
+ svgCanvas.removePath_ = removePath_
232
+ svgCanvas.getPath_ = getPath_
233
+ svgCanvas.addCtrlGrip = addCtrlGripMethod
234
+ svgCanvas.getCtrlLine = getCtrlLineMethod
235
+ svgCanvas.getGripPt = getGripPt
236
+ svgCanvas.getPointFromGrip = getPointFromGripMethod
237
+ svgCanvas.setLinkControlPoints = setLinkControlPoints
238
+ svgCanvas.reorientGrads = reorientGrads
239
+ svgCanvas.getSegData = () => { return segData }
240
+ svgCanvas.getUIStrings = () => { return uiStrings }
241
+ svgCanvas.getPathObj = () => { return path }
242
+ svgCanvas.setPathObj = (obj) => { path = obj }
243
+ svgCanvas.getPathFuncs = () => { return pathFuncs }
244
+ svgCanvas.getLinkControlPts = () => { return linkControlPts }
245
+ pathFuncs = [0, 'ClosePath']
246
+ const pathFuncsStrs = [
247
+ 'Moveto', 'Lineto', 'CurvetoCubic', 'CurvetoQuadratic', 'Arc',
248
+ 'LinetoHorizontal', 'LinetoVertical', 'CurvetoCubicSmooth', 'CurvetoQuadraticSmooth'
249
+ ]
250
+ pathFuncsStrs.forEach((s) => {
251
+ pathFuncs.push(s + 'Abs')
252
+ pathFuncs.push(s + 'Rel')
253
+ })
254
+ pathActionsInit(svgCanvas)
255
+ pathMethodInit(svgCanvas)
256
+ }
257
+
258
+ /* eslint-disable max-len */
259
+ /**
260
+ * @function module:path.ptObjToArr
261
+ * @todo See if this should just live in `replacePathSeg`
262
+ * @param {string} type
263
+ * @param {SVGPathSegMovetoAbs|SVGPathSegLinetoAbs|SVGPathSegCurvetoCubicAbs|SVGPathSegCurvetoQuadraticAbs|SVGPathSegArcAbs|SVGPathSegLinetoHorizontalAbs|SVGPathSegLinetoVerticalAbs|SVGPathSegCurvetoCubicSmoothAbs|SVGPathSegCurvetoQuadraticSmoothAbs} segItem
264
+ * @returns {ArgumentsArray}
265
+ */
266
+ /* eslint-enable max-len */
267
+ export const ptObjToArr = ptObjToArrMethod
268
+
269
+ /**
270
+ * @function module:path.getGripPt
271
+ * @param {Segment} seg
272
+ * @param {module:math.XYObject} altPt
273
+ * @returns {module:math.XYObject}
274
+ */
275
+ export const getGripPt = getGripPtMethod
276
+
277
+ /**
278
+ * @function module:path.getPointFromGrip
279
+ * @param {module:math.XYObject} pt
280
+ * @param {module:path.Path} pth
281
+ * @returns {module:math.XYObject}
282
+ */
283
+ export const getPointFromGrip = getPointFromGripMethod
284
+
285
+ /**
286
+ * Requires prior call to `setUiStrings` if `xlink:title`
287
+ * to be set on the grip.
288
+ * @function module:path.addPointGrip
289
+ * @param {Integer} index
290
+ * @param {Integer} x
291
+ * @param {Integer} y
292
+ * @returns {SVGCircleElement}
293
+ */
294
+ export const addPointGrip = addPointGripMethod
295
+
296
+ /**
297
+ * @function module:path.getGripContainer
298
+ * @returns {Element}
299
+ */
300
+ export const getGripContainer = getGripContainerMethod
301
+
302
+ /**
303
+ * Requires prior call to `setUiStrings` if `xlink:title`
304
+ * to be set on the grip.
305
+ * @function module:path.addCtrlGrip
306
+ * @param {string} id
307
+ * @returns {SVGCircleElement}
308
+ */
309
+ export const addCtrlGrip = addCtrlGripMethod
310
+
311
+ /**
312
+ * @function module:path.getCtrlLine
313
+ * @param {string} id
314
+ * @returns {SVGLineElement}
315
+ */
316
+ export const getCtrlLine = getCtrlLineMethod
317
+
318
+ /**
319
+ * @function module:path.getPointGrip
320
+ * @param {Segment} seg
321
+ * @param {boolean} update
322
+ * @returns {SVGCircleElement}
323
+ */
324
+ export const getPointGrip = getPointGripMethod
325
+
326
+ /**
327
+ * @function module:path.getControlPoints
328
+ * @param {Segment} seg
329
+ * @returns {PlainObject<string, SVGLineElement|SVGCircleElement>}
330
+ */
331
+ export const getControlPoints = getControlPointsMethod
332
+
333
+ /**
334
+ * This replaces the segment at the given index. Type is given as number.
335
+ * @function module:path.replacePathSeg
336
+ * @param {Integer} type Possible values set during {@link module:path.init}
337
+ * @param {Integer} index
338
+ * @param {ArgumentsArray} pts
339
+ * @param {SVGPathElement} elem
340
+ * @returns {void}
341
+ */
342
+ export const replacePathSeg = replacePathSegMethod
343
+
344
+ /**
345
+ * @function module:path.getSegSelector
346
+ * @param {Segment} seg
347
+ * @param {boolean} update
348
+ * @returns {SVGPathElement}
349
+ */
350
+ export const getSegSelector = getSegSelectorMethod
351
+
352
+ /**
353
+ * @typedef {PlainObject} Point
354
+ * @property {Integer} x The x value
355
+ * @property {Integer} y The y value
356
+ */
357
+
358
+ /**
359
+ * Takes three points and creates a smoother line based on them.
360
+ * @function module:path.smoothControlPoints
361
+ * @param {Point} ct1 - Object with x and y values (first control point)
362
+ * @param {Point} ct2 - Object with x and y values (second control point)
363
+ * @param {Point} pt - Object with x and y values (third point)
364
+ * @returns {Point[]} Array of two "smoothed" point objects
365
+ */
366
+ export const smoothControlPoints = (ct1, ct2, pt) => {
367
+ // each point must not be the origin
368
+ const x1 = ct1.x - pt.x
369
+ const y1 = ct1.y - pt.y
370
+ const x2 = ct2.x - pt.x
371
+ const y2 = ct2.y - pt.y
372
+
373
+ if ((x1 !== 0 || y1 !== 0) && (x2 !== 0 || y2 !== 0)) {
374
+ const
375
+ r1 = Math.sqrt(x1 * x1 + y1 * y1)
376
+ const r2 = Math.sqrt(x2 * x2 + y2 * y2)
377
+ const nct1 = svgCanvas.getSvgRoot().createSVGPoint()
378
+ const nct2 = svgCanvas.getSvgRoot().createSVGPoint()
379
+ let anglea = Math.atan2(y1, x1)
380
+ let angleb = Math.atan2(y2, x2)
381
+ if (anglea < 0) { anglea += 2 * Math.PI }
382
+ if (angleb < 0) { angleb += 2 * Math.PI }
383
+
384
+ const angleBetween = Math.abs(anglea - angleb)
385
+ const angleDiff = Math.abs(Math.PI - angleBetween) / 2
386
+
387
+ let newAnglea; let newAngleb
388
+ if (anglea - angleb > 0) {
389
+ newAnglea = angleBetween < Math.PI ? (anglea + angleDiff) : (anglea - angleDiff)
390
+ newAngleb = angleBetween < Math.PI ? (angleb - angleDiff) : (angleb + angleDiff)
391
+ } else {
392
+ newAnglea = angleBetween < Math.PI ? (anglea - angleDiff) : (anglea + angleDiff)
393
+ newAngleb = angleBetween < Math.PI ? (angleb + angleDiff) : (angleb - angleDiff)
394
+ }
395
+
396
+ // rotate the points
397
+ nct1.x = r1 * Math.cos(newAnglea) + pt.x
398
+ nct1.y = r1 * Math.sin(newAnglea) + pt.y
399
+ nct2.x = r2 * Math.cos(newAngleb) + pt.x
400
+ nct2.y = r2 * Math.sin(newAngleb) + pt.y
401
+
402
+ return [nct1, nct2]
403
+ }
404
+ return undefined
405
+ }
406
+
407
+ /**
408
+ * @function module:path.getPath_
409
+ * @param {SVGPathElement} elem
410
+ * @returns {module:path.Path}
411
+ */
412
+ export const getPath_ = (elem) => {
413
+ let p = pathData[elem.id]
414
+ if (!p) {
415
+ p = pathData[elem.id] = new Path(elem)
416
+ }
417
+ return p
418
+ }
419
+
420
+ /**
421
+ * @function module:path.removePath_
422
+ * @param {string} id
423
+ * @returns {void}
424
+ */
425
+ export const removePath_ = (id) => {
426
+ if (id in pathData) { delete pathData[id] }
427
+ }
428
+
429
+ let newcx; let newcy; let oldcx; let oldcy; let angle
430
+
431
+ const getRotVals = (x, y) => {
432
+ let dx = x - oldcx
433
+ let dy = y - oldcy
434
+
435
+ // rotate the point around the old center
436
+ let r = Math.sqrt(dx * dx + dy * dy)
437
+ let theta = Math.atan2(dy, dx) + angle
438
+ dx = r * Math.cos(theta) + oldcx
439
+ dy = r * Math.sin(theta) + oldcy
440
+
441
+ // dx,dy should now hold the actual coordinates of each
442
+ // point after being rotated
443
+
444
+ // now we want to rotate them around the new center in the reverse direction
445
+ dx -= newcx
446
+ dy -= newcy
447
+
448
+ r = Math.sqrt(dx * dx + dy * dy)
449
+ theta = Math.atan2(dy, dx) - angle
450
+
451
+ return {
452
+ x: r * Math.cos(theta) + newcx,
453
+ y: r * Math.sin(theta) + newcy
454
+ }
455
+ }
456
+
457
+ // If the path was rotated, we must now pay the piper:
458
+ // Every path point must be rotated into the rotated coordinate system of
459
+ // its old center, then determine the new center, then rotate it back
460
+ // This is because we want the path to remember its rotation
461
+
462
+ /**
463
+ * @function module:path.recalcRotatedPath
464
+ * @todo This is still using ye olde transform methods, can probably
465
+ * be optimized or even taken care of by `recalculateDimensions`
466
+ * @returns {void}
467
+ */
468
+ export const recalcRotatedPath = () => {
469
+ const currentPath = path.elem
470
+ angle = getRotationAngle(currentPath, true)
471
+ if (!angle) { return }
472
+ // selectedBBoxes[0] = path.oldbbox;
473
+ const oldbox = path.oldbbox // selectedBBoxes[0],
474
+ oldcx = oldbox.x + oldbox.width / 2
475
+ oldcy = oldbox.y + oldbox.height / 2
476
+ const box = getBBox(currentPath)
477
+ newcx = box.x + box.width / 2
478
+ newcy = box.y + box.height / 2
479
+
480
+ // un-rotate the new center to the proper position
481
+ const dx = newcx - oldcx
482
+ const dy = newcy - oldcy
483
+ const r = Math.sqrt(dx * dx + dy * dy)
484
+ const theta = Math.atan2(dy, dx) + angle
485
+
486
+ newcx = r * Math.cos(theta) + oldcx
487
+ newcy = r * Math.sin(theta) + oldcy
488
+
489
+ const list = currentPath.pathSegList
490
+
491
+ let i = list.numberOfItems
492
+ while (i) {
493
+ i -= 1
494
+ const seg = list.getItem(i)
495
+ const type = seg.pathSegType
496
+ if (type === 1) { continue }
497
+
498
+ const rvals = getRotVals(seg.x, seg.y)
499
+ const points = [rvals.x, rvals.y]
500
+ if (seg.x1 && seg.x2) {
501
+ const cVals1 = getRotVals(seg.x1, seg.y1)
502
+ const cVals2 = getRotVals(seg.x2, seg.y2)
503
+ points.splice(points.length, 0, cVals1.x, cVals1.y, cVals2.x, cVals2.y)
504
+ }
505
+ replacePathSeg(type, i, points)
506
+ } // loop for each point
507
+
508
+ /* box = */ getBBox(currentPath)
509
+ // selectedBBoxes[0].x = box.x; selectedBBoxes[0].y = box.y;
510
+ // selectedBBoxes[0].width = box.width; selectedBBoxes[0].height = box.height;
511
+
512
+ // now we must set the new transform to be rotated around the new center
513
+ const Rnc = svgCanvas.getSvgRoot().createSVGTransform()
514
+ const tlist = currentPath.transform.baseVal
515
+ Rnc.setRotate((angle * 180.0 / Math.PI), newcx, newcy)
516
+ tlist.replaceItem(Rnc, 0)
517
+ }
518
+
519
+ // ====================================
520
+ // Public API starts here
521
+
522
+ /**
523
+ * @function module:path.clearData
524
+ * @returns {void}
525
+ */
526
+ export const clearData = () => {
527
+ pathData = {}
528
+ }
529
+
530
+ // Making public for mocking
531
+ /**
532
+ * @function module:path.reorientGrads
533
+ * @param {Element} elem
534
+ * @param {SVGMatrix} m
535
+ * @returns {void}
536
+ */
537
+ export const reorientGrads = (elem, m) => {
538
+ const bb = utilsGetBBox(elem)
539
+ for (let i = 0; i < 2; i++) {
540
+ const type = i === 0 ? 'fill' : 'stroke'
541
+ const attrVal = elem.getAttribute(type)
542
+ if (attrVal && attrVal.startsWith('url(')) {
543
+ const grad = getRefElem(attrVal)
544
+ if (grad.tagName === 'linearGradient') {
545
+ let x1 = grad.getAttribute('x1') || 0
546
+ let y1 = grad.getAttribute('y1') || 0
547
+ let x2 = grad.getAttribute('x2') || 1
548
+ let y2 = grad.getAttribute('y2') || 0
549
+
550
+ // Convert to USOU points
551
+ x1 = (bb.width * x1) + bb.x
552
+ y1 = (bb.height * y1) + bb.y
553
+ x2 = (bb.width * x2) + bb.x
554
+ y2 = (bb.height * y2) + bb.y
555
+
556
+ // Transform those points
557
+ const pt1 = transformPoint(x1, y1, m)
558
+ const pt2 = transformPoint(x2, y2, m)
559
+
560
+ // Convert back to BB points
561
+ const gCoords = {
562
+ x1: (pt1.x - bb.x) / bb.width,
563
+ y1: (pt1.y - bb.y) / bb.height,
564
+ x2: (pt2.x - bb.x) / bb.width,
565
+ y2: (pt2.y - bb.y) / bb.height
566
+ }
567
+
568
+ const newgrad = grad.cloneNode(true)
569
+ for (const [key, value] of Object.entries(gCoords)) {
570
+ newgrad.setAttribute(key, value)
571
+ }
572
+ newgrad.id = svgCanvas.getNextId()
573
+ findDefs().append(newgrad)
574
+ elem.setAttribute(type, 'url(#' + newgrad.id + ')')
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ /**
581
+ * This is how we map paths to our preferred relative segment types.
582
+ * @name module:path.pathMap
583
+ * @type {GenericArray}
584
+ */
585
+ const pathMap = [
586
+ 0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
587
+ 'H', 'h', 'V', 'v', 'S', 's', 'T', 't'
588
+ ]
589
+
590
+ /**
591
+ * Convert a path to one with only absolute or relative values.
592
+ * @todo move to pathActions.js
593
+ * @function module:path.convertPath
594
+ * @param {SVGPathElement} pth - the path to convert
595
+ * @param {boolean} toRel - true of convert to relative
596
+ * @returns {string}
597
+ */
598
+ export const convertPath = (pth, toRel) => {
599
+ const { pathSegList } = pth
600
+ const len = pathSegList.numberOfItems
601
+ let curx = 0; let cury = 0
602
+ let d = ''
603
+ let lastM = null
604
+
605
+ for (let i = 0; i < len; ++i) {
606
+ const seg = pathSegList.getItem(i)
607
+ // if these properties are not in the segment, set them to zero
608
+ let x = seg.x || 0
609
+ let y = seg.y || 0
610
+ let x1 = seg.x1 || 0
611
+ let y1 = seg.y1 || 0
612
+ let x2 = seg.x2 || 0
613
+ let y2 = seg.y2 || 0
614
+
615
+ const type = seg.pathSegType
616
+ let letter = pathMap[type][toRel ? 'toLowerCase' : 'toUpperCase']()
617
+
618
+ switch (type) {
619
+ case 1: // z,Z closepath (Z/z)
620
+ d += 'z'
621
+ if (lastM && !toRel) {
622
+ curx = lastM[0]
623
+ cury = lastM[1]
624
+ }
625
+ break
626
+ case 12: // absolute horizontal line (H)
627
+ x -= curx
628
+ // Fallthrough
629
+ case 13: // relative horizontal line (h)
630
+ if (toRel) {
631
+ y = 0
632
+ curx += x
633
+ letter = 'l'
634
+ } else {
635
+ y = cury
636
+ x += curx
637
+ curx = x
638
+ letter = 'L'
639
+ }
640
+ // Convert to "line" for easier editing
641
+ d += pathDSegment(letter, [[x, y]])
642
+ break
643
+ case 14: // absolute vertical line (V)
644
+ y -= cury
645
+ // Fallthrough
646
+ case 15: // relative vertical line (v)
647
+ if (toRel) {
648
+ x = 0
649
+ cury += y
650
+ letter = 'l'
651
+ } else {
652
+ x = curx
653
+ y += cury
654
+ cury = y
655
+ letter = 'L'
656
+ }
657
+ // Convert to "line" for easier editing
658
+ d += pathDSegment(letter, [[x, y]])
659
+ break
660
+ case 2: // absolute move (M)
661
+ case 4: // absolute line (L)
662
+ case 18: // absolute smooth quad (T)
663
+ case 10: // absolute elliptical arc (A)
664
+ x -= curx
665
+ y -= cury
666
+ // Fallthrough
667
+ case 5: // relative line (l)
668
+ case 3: // relative move (m)
669
+ case 19: // relative smooth quad (t)
670
+ if (toRel) {
671
+ curx += x
672
+ cury += y
673
+ } else {
674
+ x += curx
675
+ y += cury
676
+ curx = x
677
+ cury = y
678
+ }
679
+ if (type === 2 || type === 3) { lastM = [curx, cury] }
680
+
681
+ d += pathDSegment(letter, [[x, y]])
682
+ break
683
+ case 6: // absolute cubic (C)
684
+ x -= curx; x1 -= curx; x2 -= curx
685
+ y -= cury; y1 -= cury; y2 -= cury
686
+ // Fallthrough
687
+ case 7: // relative cubic (c)
688
+ if (toRel) {
689
+ curx += x
690
+ cury += y
691
+ } else {
692
+ x += curx; x1 += curx; x2 += curx
693
+ y += cury; y1 += cury; y2 += cury
694
+ curx = x
695
+ cury = y
696
+ }
697
+ d += pathDSegment(letter, [[x1, y1], [x2, y2], [x, y]])
698
+ break
699
+ case 8: // absolute quad (Q)
700
+ x -= curx; x1 -= curx
701
+ y -= cury; y1 -= cury
702
+ // Fallthrough
703
+ case 9: // relative quad (q)
704
+ if (toRel) {
705
+ curx += x
706
+ cury += y
707
+ } else {
708
+ x += curx; x1 += curx
709
+ y += cury; y1 += cury
710
+ curx = x
711
+ cury = y
712
+ }
713
+ d += pathDSegment(letter, [[x1, y1], [x, y]])
714
+ break
715
+ // Fallthrough
716
+ case 11: // relative elliptical arc (a)
717
+ if (toRel) {
718
+ curx += x
719
+ cury += y
720
+ } else {
721
+ x += curx
722
+ y += cury
723
+ curx = x
724
+ cury = y
725
+ }
726
+ d += pathDSegment(letter, [[seg.r1, seg.r2]], [
727
+ seg.angle,
728
+ (seg.largeArcFlag ? 1 : 0),
729
+ (seg.sweepFlag ? 1 : 0)
730
+ ], [x, y])
731
+ break
732
+ case 16: // absolute smooth cubic (S)
733
+ x -= curx; x2 -= curx
734
+ y -= cury; y2 -= cury
735
+ // Fallthrough
736
+ case 17: // relative smooth cubic (s)
737
+ if (toRel) {
738
+ curx += x
739
+ cury += y
740
+ } else {
741
+ x += curx; x2 += curx
742
+ y += cury; y2 += cury
743
+ curx = x
744
+ cury = y
745
+ }
746
+ d += pathDSegment(letter, [[x2, y2], [x, y]])
747
+ break
748
+ } // switch on path segment type
749
+ } // for each segment
750
+ return d
751
+ }
752
+
753
+ /**
754
+ * TODO: refactor callers in `convertPath` to use `getPathDFromSegments` instead of this function.
755
+ * Legacy code refactored from `svgcanvas.pathActions.convertPath`.
756
+ * @param {string} letter - path segment command (letter in potentially either case from {@link module:path.pathMap}; see [SVGPathSeg#pathSegTypeAsLetter]{@link https://www.w3.org/TR/SVG/single-page.html#paths-__svg__SVGPathSeg__pathSegTypeAsLetter})
757
+ * @param {GenericArray<GenericArray<Integer>>} points - x,y points
758
+ * @param {GenericArray<GenericArray<Integer>>} [morePoints] - x,y points
759
+ * @param {Integer[]} [lastPoint] - x,y point
760
+ * @returns {string}
761
+ */
762
+ const pathDSegment = (letter, points, morePoints, lastPoint) => {
763
+ points.forEach((pnt, i) => {
764
+ points[i] = shortFloat(pnt)
765
+ })
766
+ let segment = letter + points.join(' ')
767
+ if (morePoints) {
768
+ segment += ' ' + morePoints.join(' ')
769
+ }
770
+ if (lastPoint) {
771
+ segment += ' ' + shortFloat(lastPoint)
772
+ }
773
+ return segment
774
+ }
775
+
776
+ /**
777
+ * Group: Path edit functions.
778
+ * Functions relating to editing path elements.
779
+ */
780
+ export const pathActions = pathActionsMethod
781
+ // end pathActions