@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.
@@ -0,0 +1,1237 @@
1
+ /**
2
+ * Path functionality.
3
+ * @module path
4
+ * @license MIT
5
+ *
6
+ * @copyright 2011 Alexis Deveria, 2011 Jeff Schiller
7
+ */
8
+
9
+ import { NS } from './namespaces.js'
10
+ import { shortFloat } from '../../src/common/units.js'
11
+ import { ChangeElementCommand, BatchCommand } from './history.js'
12
+ import {
13
+ transformPoint, snapToAngle, rectsIntersect,
14
+ transformListToTransform
15
+ } from './math.js'
16
+ import {
17
+ assignAttributes, getElement, getRotationAngle, snapToGrid,
18
+ getBBox
19
+ } from './utilities.js'
20
+
21
+ let svgCanvas = null
22
+ let path = null
23
+
24
+ /**
25
+ * @function module:path-actions.init
26
+ * @param {module:path-actions.svgCanvas} pathActionsContext
27
+ * @returns {void}
28
+ */
29
+ export const init = (canvas) => {
30
+ svgCanvas = canvas
31
+ }
32
+
33
+ /**
34
+ * Convert a path to one with only absolute or relative values.
35
+ * @todo move to pathActions.js
36
+ * @function module:path.convertPath
37
+ * @param {SVGPathElement} pth - the path to convert
38
+ * @param {boolean} toRel - true of convert to relative
39
+ * @returns {string}
40
+ */
41
+ export const convertPath = function (pth, toRel) {
42
+ const { pathSegList } = pth
43
+ const len = pathSegList.numberOfItems
44
+ let curx = 0; let cury = 0
45
+ let d = ''
46
+ let lastM = null
47
+
48
+ for (let i = 0; i < len; ++i) {
49
+ const seg = pathSegList.getItem(i)
50
+ // if these properties are not in the segment, set them to zero
51
+ let x = seg.x || 0
52
+ let y = seg.y || 0
53
+ let x1 = seg.x1 || 0
54
+ let y1 = seg.y1 || 0
55
+ let x2 = seg.x2 || 0
56
+ let y2 = seg.y2 || 0
57
+
58
+ // const type = seg.pathSegType;
59
+ // const pathMap = svgCanvas.getPathMap();
60
+ // let letter = pathMap[type][toRel ? 'toLowerCase' : 'toUpperCase']();
61
+ let letter = seg.pathSegTypeAsLetter
62
+
63
+ switch (letter) {
64
+ case 'z': // z,Z closepath (Z/z)
65
+ case 'Z':
66
+ d += 'z'
67
+ if (lastM && !toRel) {
68
+ curx = lastM[0]
69
+ cury = lastM[1]
70
+ }
71
+ break
72
+ case 'H': // absolute horizontal line (H)
73
+ x -= curx
74
+ // Fallthrough
75
+ case 'h': // relative horizontal line (h)
76
+ if (toRel) {
77
+ y = 0
78
+ curx += x
79
+ letter = 'l'
80
+ } else {
81
+ y = cury
82
+ x += curx
83
+ curx = x
84
+ letter = 'L'
85
+ }
86
+ // Convert to "line" for easier editing
87
+ d += pathDSegment(letter, [[x, y]])
88
+ break
89
+ case 'V': // absolute vertical line (V)
90
+ y -= cury
91
+ // Fallthrough
92
+ case 'v': // relative vertical line (v)
93
+ if (toRel) {
94
+ x = 0
95
+ cury += y
96
+ letter = 'l'
97
+ } else {
98
+ x = curx
99
+ y += cury
100
+ cury = y
101
+ letter = 'L'
102
+ }
103
+ // Convert to "line" for easier editing
104
+ d += pathDSegment(letter, [[x, y]])
105
+ break
106
+ case 'M': // absolute move (M)
107
+ case 'L': // absolute line (L)
108
+ case 'T': // absolute smooth quad (T)
109
+ x -= curx
110
+ y -= cury
111
+ // Fallthrough
112
+ case 'l': // relative line (l)
113
+ case 'm': // relative move (m)
114
+ case 't': // relative smooth quad (t)
115
+ if (toRel) {
116
+ curx += x
117
+ cury += y
118
+ letter = letter.toLowerCase()
119
+ } else {
120
+ x += curx
121
+ y += cury
122
+ curx = x
123
+ cury = y
124
+ letter = letter.toUpperCase()
125
+ }
126
+ if (letter === 'm' || letter === 'M') { lastM = [curx, cury] }
127
+
128
+ d += pathDSegment(letter, [[x, y]])
129
+ break
130
+ case 'C': // absolute cubic (C)
131
+ x -= curx; x1 -= curx; x2 -= curx
132
+ y -= cury; y1 -= cury; y2 -= cury
133
+ // Fallthrough
134
+ case 'c': // relative cubic (c)
135
+ if (toRel) {
136
+ curx += x
137
+ cury += y
138
+ letter = 'c'
139
+ } else {
140
+ x += curx; x1 += curx; x2 += curx
141
+ y += cury; y1 += cury; y2 += cury
142
+ curx = x
143
+ cury = y
144
+ letter = 'C'
145
+ }
146
+ d += pathDSegment(letter, [[x1, y1], [x2, y2], [x, y]])
147
+ break
148
+ case 'Q': // absolute quad (Q)
149
+ x -= curx; x1 -= curx
150
+ y -= cury; y1 -= cury
151
+ // Fallthrough
152
+ case 'q': // relative quad (q)
153
+ if (toRel) {
154
+ curx += x
155
+ cury += y
156
+ letter = 'q'
157
+ } else {
158
+ x += curx; x1 += curx
159
+ y += cury; y1 += cury
160
+ curx = x
161
+ cury = y
162
+ letter = 'Q'
163
+ }
164
+ d += pathDSegment(letter, [[x1, y1], [x, y]])
165
+ break
166
+ case 'A':
167
+ x -= curx
168
+ y -= cury
169
+ // fallthrough
170
+ case 'a': // relative elliptical arc (a)
171
+ if (toRel) {
172
+ curx += x
173
+ cury += y
174
+ letter = 'a'
175
+ } else {
176
+ x += curx
177
+ y += cury
178
+ curx = x
179
+ cury = y
180
+ letter = 'A'
181
+ }
182
+ d += pathDSegment(letter, [[seg.r1, seg.r2]], [
183
+ seg.angle,
184
+ (seg.largeArcFlag ? 1 : 0),
185
+ (seg.sweepFlag ? 1 : 0)
186
+ ], [x, y])
187
+ break
188
+ case 'S': // absolute smooth cubic (S)
189
+ x -= curx; x2 -= curx
190
+ y -= cury; y2 -= cury
191
+ // Fallthrough
192
+ case 's': // relative smooth cubic (s)
193
+ if (toRel) {
194
+ curx += x
195
+ cury += y
196
+ letter = 's'
197
+ } else {
198
+ x += curx; x2 += curx
199
+ y += cury; y2 += cury
200
+ curx = x
201
+ cury = y
202
+ letter = 'S'
203
+ }
204
+ d += pathDSegment(letter, [[x2, y2], [x, y]])
205
+ break
206
+ } // switch on path segment type
207
+ } // for each segment
208
+ return d
209
+ }
210
+
211
+ /**
212
+ * TODO: refactor callers in `convertPath` to use `getPathDFromSegments` instead of this function.
213
+ * Legacy code refactored from `svgcanvas.pathActions.convertPath`.
214
+ * @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})
215
+ * @param {GenericArray<GenericArray<Integer>>} points - x,y points
216
+ * @param {GenericArray<GenericArray<Integer>>} [morePoints] - x,y points
217
+ * @param {Integer[]} [lastPoint] - x,y point
218
+ * @returns {string}
219
+ */
220
+ function pathDSegment (letter, points, morePoints, lastPoint) {
221
+ points.forEach(function (pnt, i) {
222
+ points[i] = shortFloat(pnt)
223
+ })
224
+ let segment = letter + points.join(' ')
225
+ if (morePoints) {
226
+ segment += ' ' + morePoints.join(' ')
227
+ }
228
+ if (lastPoint) {
229
+ segment += ' ' + shortFloat(lastPoint)
230
+ }
231
+ return segment
232
+ }
233
+
234
+ /**
235
+ * Group: Path edit functions.
236
+ * Functions relating to editing path elements.
237
+ * @namespace {PlainObject} pathActions
238
+ * @memberof module:path
239
+ */
240
+ export const pathActionsMethod = (function () {
241
+ let subpath = false
242
+ let newPoint; let firstCtrl
243
+
244
+ let currentPath = null
245
+ let hasMoved = false
246
+ // No `svgCanvas` yet but should be ok as is `null` by default
247
+ // svgCanvas.setDrawnPath(null);
248
+
249
+ /**
250
+ * This function converts a polyline (created by the fh_path tool) into
251
+ * a path element and coverts every three line segments into a single bezier
252
+ * curve in an attempt to smooth out the free-hand.
253
+ * @function smoothPolylineIntoPath
254
+ * @param {Element} element
255
+ * @returns {Element}
256
+ */
257
+ const smoothPolylineIntoPath = function (element) {
258
+ let i
259
+ const { points } = element
260
+ const N = points.numberOfItems
261
+ if (N >= 4) {
262
+ // loop through every 3 points and convert to a cubic bezier curve segment
263
+ //
264
+ // NOTE: this is cheating, it means that every 3 points has the potential to
265
+ // be a corner instead of treating each point in an equal manner. In general,
266
+ // this technique does not look that good.
267
+ //
268
+ // I am open to better ideas!
269
+ //
270
+ // Reading:
271
+ // - http://www.efg2.com/Lab/Graphics/Jean-YvesQueinecBezierCurves.htm
272
+ // - https://www.codeproject.com/KB/graphics/BezierSpline.aspx?msg=2956963
273
+ // - https://www.ian-ko.com/ET_GeoWizards/UserGuide/smooth.htm
274
+ // - https://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html
275
+ let curpos = points.getItem(0); let prevCtlPt = null
276
+ let d = []
277
+ d.push(['M', curpos.x, ',', curpos.y, ' C'].join(''))
278
+ for (i = 1; i <= (N - 4); i += 3) {
279
+ let ct1 = points.getItem(i)
280
+ const ct2 = points.getItem(i + 1)
281
+ const end = points.getItem(i + 2)
282
+
283
+ // if the previous segment had a control point, we want to smooth out
284
+ // the control points on both sides
285
+ if (prevCtlPt) {
286
+ const newpts = svgCanvas.smoothControlPoints(prevCtlPt, ct1, curpos)
287
+ if (newpts?.length === 2) {
288
+ const prevArr = d[d.length - 1].split(',')
289
+ prevArr[2] = newpts[0].x
290
+ prevArr[3] = newpts[0].y
291
+ d[d.length - 1] = prevArr.join(',')
292
+ ct1 = newpts[1]
293
+ }
294
+ }
295
+
296
+ d.push([ct1.x, ct1.y, ct2.x, ct2.y, end.x, end.y].join(','))
297
+
298
+ curpos = end
299
+ prevCtlPt = ct2
300
+ }
301
+ // handle remaining line segments
302
+ d.push('L')
303
+ while (i < N) {
304
+ const pt = points.getItem(i)
305
+ d.push([pt.x, pt.y].join(','))
306
+ i++
307
+ }
308
+ d = d.join(' ')
309
+
310
+ element = svgCanvas.addSVGElementsFromJson({
311
+ element: 'path',
312
+ curStyles: true,
313
+ attr: {
314
+ id: svgCanvas.getId(),
315
+ d,
316
+ fill: 'none'
317
+ }
318
+ })
319
+ // No need to call "changed", as this is already done under mouseUp
320
+ }
321
+ return element
322
+ }
323
+
324
+ return (/** @lends module:path.pathActions */ {
325
+ /**
326
+ * @param {MouseEvent} evt
327
+ * @param {Element} mouseTarget
328
+ * @param {Float} startX
329
+ * @param {Float} startY
330
+ * @returns {boolean|void}
331
+ */
332
+ mouseDown (evt, mouseTarget, startX, startY) {
333
+ let id
334
+ if (svgCanvas.getCurrentMode() === 'path') {
335
+ let mouseX = startX // Was this meant to work with the other `mouseX`? (was defined globally so adding `let` to at least avoid a global)
336
+ let mouseY = startY // Was this meant to work with the other `mouseY`? (was defined globally so adding `let` to at least avoid a global)
337
+
338
+ const zoom = svgCanvas.getZoom()
339
+ let x = mouseX / zoom
340
+ let y = mouseY / zoom
341
+ let stretchy = getElement('path_stretch_line')
342
+ newPoint = [x, y]
343
+
344
+ if (svgCanvas.getGridSnapping()) {
345
+ x = snapToGrid(x)
346
+ y = snapToGrid(y)
347
+ mouseX = snapToGrid(mouseX)
348
+ mouseY = snapToGrid(mouseY)
349
+ }
350
+
351
+ if (!stretchy) {
352
+ stretchy = document.createElementNS(NS.SVG, 'path')
353
+ assignAttributes(stretchy, {
354
+ id: 'path_stretch_line',
355
+ stroke: '#22C',
356
+ 'stroke-width': '0.5',
357
+ fill: 'none'
358
+ })
359
+ getElement('selectorParentGroup').append(stretchy)
360
+ }
361
+ stretchy.setAttribute('display', 'inline')
362
+
363
+ let keep = null
364
+ let index
365
+ // if pts array is empty, create path element with M at current point
366
+ const drawnPath = svgCanvas.getDrawnPath()
367
+ if (!drawnPath) {
368
+ const dAttr = 'M' + x + ',' + y + ' ' // Was this meant to work with the other `dAttr`? (was defined globally so adding `var` to at least avoid a global)
369
+ /* drawnPath = */ svgCanvas.setDrawnPath(svgCanvas.addSVGElementsFromJson({
370
+ element: 'path',
371
+ curStyles: true,
372
+ attr: {
373
+ d: dAttr,
374
+ id: svgCanvas.getNextId(),
375
+ opacity: svgCanvas.getOpacity() / 2
376
+ }
377
+ }))
378
+ // set stretchy line to first point
379
+ stretchy.setAttribute('d', ['M', mouseX, mouseY, mouseX, mouseY].join(' '))
380
+ index = subpath ? path.segs.length : 0
381
+ svgCanvas.addPointGrip(index, mouseX, mouseY)
382
+ } else {
383
+ // determine if we clicked on an existing point
384
+ const seglist = drawnPath.pathSegList
385
+ let i = seglist.numberOfItems
386
+ const FUZZ = 6 / zoom
387
+ let clickOnPoint = false
388
+ while (i) {
389
+ i--
390
+ const item = seglist.getItem(i)
391
+ const px = item.x; const py = item.y
392
+ // found a matching point
393
+ if (x >= (px - FUZZ) && x <= (px + FUZZ) &&
394
+ y >= (py - FUZZ) && y <= (py + FUZZ)
395
+ ) {
396
+ clickOnPoint = true
397
+ break
398
+ }
399
+ }
400
+
401
+ // get path element that we are in the process of creating
402
+ id = svgCanvas.getId()
403
+
404
+ // Remove previous path object if previously created
405
+ svgCanvas.removePath_(id)
406
+
407
+ const newpath = getElement(id)
408
+ let newseg
409
+ let sSeg
410
+ const len = seglist.numberOfItems
411
+ // if we clicked on an existing point, then we are done this path, commit it
412
+ // (i, i+1) are the x,y that were clicked on
413
+ if (clickOnPoint) {
414
+ // if clicked on any other point but the first OR
415
+ // the first point was clicked on and there are less than 3 points
416
+ // then leave the path open
417
+ // otherwise, close the path
418
+ if (i <= 1 && len >= 2) {
419
+ // Create end segment
420
+ const absX = seglist.getItem(0).x
421
+ const absY = seglist.getItem(0).y
422
+
423
+ sSeg = stretchy.pathSegList.getItem(1)
424
+ newseg = sSeg.pathSegType === 4
425
+ ? drawnPath.createSVGPathSegLinetoAbs(absX, absY)
426
+ : drawnPath.createSVGPathSegCurvetoCubicAbs(absX, absY, sSeg.x1 / zoom, sSeg.y1 / zoom, absX, absY)
427
+
428
+ const endseg = drawnPath.createSVGPathSegClosePath()
429
+ seglist.appendItem(newseg)
430
+ seglist.appendItem(endseg)
431
+ } else if (len < 3) {
432
+ keep = false
433
+ return keep
434
+ }
435
+ stretchy.remove()
436
+
437
+ // This will signal to commit the path
438
+ // const element = newpath; // Other event handlers define own `element`, so this was probably not meant to interact with them or one which shares state (as there were none); I therefore adding a missing `var` to avoid a global
439
+ /* drawnPath = */ svgCanvas.setDrawnPath(null)
440
+ svgCanvas.setStarted(false)
441
+
442
+ if (subpath) {
443
+ if (path.matrix) {
444
+ svgCanvas.remapElement(newpath, {}, path.matrix.inverse())
445
+ }
446
+
447
+ const newD = newpath.getAttribute('d')
448
+ const origD = path.elem.getAttribute('d')
449
+ path.elem.setAttribute('d', origD + newD)
450
+ newpath.parentNode.removeChild(newpath)
451
+ if (path.matrix) {
452
+ svgCanvas.recalcRotatedPath()
453
+ }
454
+ pathActionsMethod.toEditMode(path.elem)
455
+ path.selectPt()
456
+ return false
457
+ }
458
+ // else, create a new point, update path element
459
+ } else {
460
+ // Checks if current target or parents are #svgcontent
461
+ if (!(svgCanvas.getContainer() !== svgCanvas.getMouseTarget(evt) && svgCanvas.getContainer().contains(
462
+ svgCanvas.getMouseTarget(evt)
463
+ ))) {
464
+ // Clicked outside canvas, so don't make point
465
+ return false
466
+ }
467
+
468
+ const num = drawnPath.pathSegList.numberOfItems
469
+ const last = drawnPath.pathSegList.getItem(num - 1)
470
+ const lastx = last.x; const lasty = last.y
471
+
472
+ if (evt.shiftKey) {
473
+ const xya = snapToAngle(lastx, lasty, x, y);
474
+ ({ x, y } = xya)
475
+ }
476
+
477
+ // Use the segment defined by stretchy
478
+ sSeg = stretchy.pathSegList.getItem(1)
479
+ newseg = sSeg.pathSegType === 4
480
+ ? drawnPath.createSVGPathSegLinetoAbs(svgCanvas.round(x), svgCanvas.round(y))
481
+ : drawnPath.createSVGPathSegCurvetoCubicAbs(
482
+ svgCanvas.round(x),
483
+ svgCanvas.round(y),
484
+ sSeg.x1 / zoom,
485
+ sSeg.y1 / zoom,
486
+ sSeg.x2 / zoom,
487
+ sSeg.y2 / zoom
488
+ )
489
+
490
+ drawnPath.pathSegList.appendItem(newseg)
491
+
492
+ x *= zoom
493
+ y *= zoom
494
+
495
+ // set stretchy line to latest point
496
+ stretchy.setAttribute('d', ['M', x, y, x, y].join(' '))
497
+ index = num
498
+ if (subpath) { index += path.segs.length }
499
+ svgCanvas.addPointGrip(index, x, y)
500
+ }
501
+ // keep = true;
502
+ }
503
+
504
+ return undefined
505
+ }
506
+
507
+ // TODO: Make sure currentPath isn't null at this point
508
+ if (!path) { return undefined }
509
+
510
+ path.storeD();
511
+
512
+ ({ id } = evt.target)
513
+ let curPt
514
+ if (id.substr(0, 14) === 'pathpointgrip_') {
515
+ // Select this point
516
+ curPt = path.cur_pt = Number.parseInt(id.substr(14))
517
+ path.dragging = [startX, startY]
518
+ const seg = path.segs[curPt]
519
+
520
+ // only clear selection if shift is not pressed (otherwise, add
521
+ // node to selection)
522
+ if (!evt.shiftKey) {
523
+ if (path.selected_pts.length <= 1 || !seg.selected) {
524
+ path.clearSelection()
525
+ }
526
+ path.addPtsToSelection(curPt)
527
+ } else if (seg.selected) {
528
+ path.removePtFromSelection(curPt)
529
+ } else {
530
+ path.addPtsToSelection(curPt)
531
+ }
532
+ } else if (id.startsWith('ctrlpointgrip_')) {
533
+ path.dragging = [startX, startY]
534
+
535
+ const parts = id.split('_')[1].split('c')
536
+ curPt = Number(parts[0])
537
+ const ctrlNum = Number(parts[1])
538
+ path.selectPt(curPt, ctrlNum)
539
+ }
540
+
541
+ // Start selection box
542
+ if (!path.dragging) {
543
+ let rubberBox = svgCanvas.getRubberBox()
544
+ if (!rubberBox) {
545
+ rubberBox = svgCanvas.setRubberBox(
546
+ svgCanvas.selectorManager.getRubberBandBox()
547
+ )
548
+ }
549
+ const zoom = svgCanvas.getZoom()
550
+ assignAttributes(rubberBox, {
551
+ x: startX * zoom,
552
+ y: startY * zoom,
553
+ width: 0,
554
+ height: 0,
555
+ display: 'inline'
556
+ }, 100)
557
+ }
558
+ return undefined
559
+ },
560
+ /**
561
+ * @param {Float} mouseX
562
+ * @param {Float} mouseY
563
+ * @returns {void}
564
+ */
565
+ mouseMove (mouseX, mouseY) {
566
+ const zoom = svgCanvas.getZoom()
567
+ hasMoved = true
568
+ const drawnPath = svgCanvas.getDrawnPath()
569
+ if (svgCanvas.getCurrentMode() === 'path') {
570
+ if (!drawnPath) { return }
571
+ const seglist = drawnPath.pathSegList
572
+ const index = seglist.numberOfItems - 1
573
+
574
+ if (newPoint) {
575
+ // First point
576
+ // if (!index) { return; }
577
+
578
+ // Set control points
579
+ const pointGrip1 = svgCanvas.addCtrlGrip('1c1')
580
+ const pointGrip2 = svgCanvas.addCtrlGrip('0c2')
581
+
582
+ // dragging pointGrip1
583
+ pointGrip1.setAttribute('cx', mouseX)
584
+ pointGrip1.setAttribute('cy', mouseY)
585
+ pointGrip1.setAttribute('display', 'inline')
586
+
587
+ const ptX = newPoint[0]
588
+ const ptY = newPoint[1]
589
+
590
+ // set curve
591
+ // const seg = seglist.getItem(index);
592
+ const curX = mouseX / zoom
593
+ const curY = mouseY / zoom
594
+ const altX = (ptX + (ptX - curX))
595
+ const altY = (ptY + (ptY - curY))
596
+
597
+ pointGrip2.setAttribute('cx', altX * zoom)
598
+ pointGrip2.setAttribute('cy', altY * zoom)
599
+ pointGrip2.setAttribute('display', 'inline')
600
+
601
+ const ctrlLine = svgCanvas.getCtrlLine(1)
602
+ assignAttributes(ctrlLine, {
603
+ x1: mouseX,
604
+ y1: mouseY,
605
+ x2: altX * zoom,
606
+ y2: altY * zoom,
607
+ display: 'inline'
608
+ })
609
+
610
+ if (index === 0) {
611
+ firstCtrl = [mouseX, mouseY]
612
+ } else {
613
+ const last = seglist.getItem(index - 1)
614
+ let lastX = last.x
615
+ let lastY = last.y
616
+
617
+ if (last.pathSegType === 6) {
618
+ lastX += (lastX - last.x2)
619
+ lastY += (lastY - last.y2)
620
+ } else if (firstCtrl) {
621
+ lastX = firstCtrl[0] / zoom
622
+ lastY = firstCtrl[1] / zoom
623
+ }
624
+ svgCanvas.replacePathSeg(6, index, [ptX, ptY, lastX, lastY, altX, altY], drawnPath)
625
+ }
626
+ } else {
627
+ const stretchy = getElement('path_stretch_line')
628
+ if (stretchy) {
629
+ const prev = seglist.getItem(index)
630
+ if (prev.pathSegType === 6) {
631
+ const prevX = prev.x + (prev.x - prev.x2)
632
+ const prevY = prev.y + (prev.y - prev.y2)
633
+ svgCanvas.replacePathSeg(
634
+ 6,
635
+ 1,
636
+ [mouseX, mouseY, prevX * zoom, prevY * zoom, mouseX, mouseY],
637
+ stretchy
638
+ )
639
+ } else if (firstCtrl) {
640
+ svgCanvas.replacePathSeg(6, 1, [mouseX, mouseY, firstCtrl[0], firstCtrl[1], mouseX, mouseY], stretchy)
641
+ } else {
642
+ svgCanvas.replacePathSeg(4, 1, [mouseX, mouseY], stretchy)
643
+ }
644
+ }
645
+ }
646
+ return
647
+ }
648
+ // if we are dragging a point, let's move it
649
+ if (path.dragging) {
650
+ const pt = svgCanvas.getPointFromGrip({
651
+ x: path.dragging[0],
652
+ y: path.dragging[1]
653
+ }, path)
654
+ const mpt = svgCanvas.getPointFromGrip({
655
+ x: mouseX,
656
+ y: mouseY
657
+ }, path)
658
+ const diffX = mpt.x - pt.x
659
+ const diffY = mpt.y - pt.y
660
+ path.dragging = [mouseX, mouseY]
661
+
662
+ if (path.dragctrl) {
663
+ path.moveCtrl(diffX, diffY)
664
+ } else {
665
+ path.movePts(diffX, diffY)
666
+ }
667
+ } else {
668
+ path.selected_pts = []
669
+ path.eachSeg(function (_i) {
670
+ const seg = this
671
+ if (!seg.next && !seg.prev) { return }
672
+
673
+ // const {item} = seg;
674
+ const rubberBox = svgCanvas.getRubberBox()
675
+ const rbb = getBBox(rubberBox)
676
+
677
+ const pt = svgCanvas.getGripPt(seg)
678
+ const ptBb = {
679
+ x: pt.x,
680
+ y: pt.y,
681
+ width: 0,
682
+ height: 0
683
+ }
684
+
685
+ const sel = rectsIntersect(rbb, ptBb)
686
+
687
+ this.select(sel)
688
+ // Note that addPtsToSelection is not being run
689
+ if (sel) { path.selected_pts.push(seg.index) }
690
+ })
691
+ }
692
+ },
693
+ /**
694
+ * @typedef module:path.keepElement
695
+ * @type {PlainObject}
696
+ * @property {boolean} keep
697
+ * @property {Element} element
698
+ */
699
+ /**
700
+ * @param {Event} evt
701
+ * @param {Element} element
702
+ * @param {Float} _mouseX
703
+ * @param {Float} _mouseY
704
+ * @returns {module:path.keepElement|void}
705
+ */
706
+ mouseUp (evt, element, _mouseX, _mouseY) {
707
+ const drawnPath = svgCanvas.getDrawnPath()
708
+ // Create mode
709
+ if (svgCanvas.getCurrentMode() === 'path') {
710
+ newPoint = null
711
+ if (!drawnPath) {
712
+ element = getElement(svgCanvas.getId())
713
+ svgCanvas.setStarted(false)
714
+ firstCtrl = null
715
+ }
716
+
717
+ return {
718
+ keep: true,
719
+ element
720
+ }
721
+ }
722
+
723
+ // Edit mode
724
+ const rubberBox = svgCanvas.getRubberBox()
725
+ if (path.dragging) {
726
+ const lastPt = path.cur_pt
727
+
728
+ path.dragging = false
729
+ path.dragctrl = false
730
+ path.update()
731
+
732
+ if (hasMoved) {
733
+ path.endChanges('Move path point(s)')
734
+ }
735
+
736
+ if (!evt.shiftKey && !hasMoved) {
737
+ path.selectPt(lastPt)
738
+ }
739
+ } else if (rubberBox?.getAttribute('display') !== 'none') {
740
+ // Done with multi-node-select
741
+ rubberBox.setAttribute('display', 'none')
742
+
743
+ if (rubberBox.getAttribute('width') <= 2 && rubberBox.getAttribute('height') <= 2) {
744
+ pathActionsMethod.toSelectMode(evt.target)
745
+ }
746
+
747
+ // else, move back to select mode
748
+ } else {
749
+ pathActionsMethod.toSelectMode(evt.target)
750
+ }
751
+ hasMoved = false
752
+ return undefined
753
+ },
754
+ /**
755
+ * @param {Element} element
756
+ * @returns {void}
757
+ */
758
+ toEditMode (element) {
759
+ path = svgCanvas.getPath_(element)
760
+ svgCanvas.setCurrentMode('pathedit')
761
+ svgCanvas.clearSelection()
762
+ path.setPathContext()
763
+ path.show(true).update()
764
+ path.oldbbox = getBBox(path.elem)
765
+ subpath = false
766
+ },
767
+ /**
768
+ * @param {Element} elem
769
+ * @fires module:svgcanvas.SvgCanvas#event:selected
770
+ * @returns {void}
771
+ */
772
+ toSelectMode (elem) {
773
+ const selPath = (elem === path.elem)
774
+ svgCanvas.setCurrentMode('select')
775
+ path.setPathContext()
776
+ path.show(false)
777
+ currentPath = false
778
+ svgCanvas.clearSelection()
779
+
780
+ if (path.matrix) {
781
+ // Rotated, so may need to re-calculate the center
782
+ svgCanvas.recalcRotatedPath()
783
+ }
784
+
785
+ if (selPath) {
786
+ svgCanvas.call('selected', [elem])
787
+ svgCanvas.addToSelection([elem], true)
788
+ }
789
+ },
790
+ /**
791
+ * @param {boolean} on
792
+ * @returns {void}
793
+ */
794
+ addSubPath (on) {
795
+ if (on) {
796
+ // Internally we go into "path" mode, but in the UI it will
797
+ // still appear as if in "pathedit" mode.
798
+ svgCanvas.setCurrentMode('path')
799
+ subpath = true
800
+ } else {
801
+ pathActionsMethod.clear(true)
802
+ pathActionsMethod.toEditMode(path.elem)
803
+ }
804
+ },
805
+ /**
806
+ * @param {Element} target
807
+ * @returns {void}
808
+ */
809
+ select (target) {
810
+ if (currentPath === target) {
811
+ pathActionsMethod.toEditMode(target)
812
+ svgCanvas.setCurrentMode('pathedit')
813
+ // going into pathedit mode
814
+ } else {
815
+ currentPath = target
816
+ }
817
+ },
818
+ /**
819
+ * @fires module:svgcanvas.SvgCanvas#event:changed
820
+ * @returns {void}
821
+ */
822
+ reorient () {
823
+ const elem = svgCanvas.getSelectedElements()[0]
824
+ if (!elem) { return }
825
+ const angl = getRotationAngle(elem)
826
+ if (angl === 0) { return }
827
+
828
+ const batchCmd = new BatchCommand('Reorient path')
829
+ const changes = {
830
+ d: elem.getAttribute('d'),
831
+ transform: elem.getAttribute('transform')
832
+ }
833
+ batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
834
+ svgCanvas.clearSelection()
835
+ this.resetOrientation(elem)
836
+
837
+ svgCanvas.addCommandToHistory(batchCmd)
838
+
839
+ // Set matrix to null
840
+ svgCanvas.getPath_(elem).show(false).matrix = null
841
+
842
+ this.clear()
843
+
844
+ svgCanvas.addToSelection([elem], true)
845
+ svgCanvas.call('changed', svgCanvas.getSelectedElements())
846
+ },
847
+
848
+ /**
849
+ * @param {boolean} remove Not in use
850
+ * @returns {void}
851
+ */
852
+ clear () {
853
+ const drawnPath = svgCanvas.getDrawnPath()
854
+ currentPath = null
855
+ if (drawnPath) {
856
+ const elem = getElement(svgCanvas.getId())
857
+ const psl = getElement('path_stretch_line')
858
+ psl.parentNode.removeChild(psl)
859
+ elem.parentNode.removeChild(elem)
860
+ const pathpointgripContainer = getElement('pathpointgrip_container')
861
+ const elements = pathpointgripContainer.querySelectorAll('*')
862
+ Array.prototype.forEach.call(elements, function (el) {
863
+ el.setAttribute('display', 'none')
864
+ })
865
+ firstCtrl = null
866
+ svgCanvas.setDrawnPath(null)
867
+ svgCanvas.setStarted(false)
868
+ } else if (svgCanvas.getCurrentMode() === 'pathedit') {
869
+ this.toSelectMode()
870
+ }
871
+ if (path) { path.init().show(false) }
872
+ },
873
+ /**
874
+ * @param {?(Element|SVGPathElement)} pth
875
+ * @returns {false|void}
876
+ */
877
+ resetOrientation (pth) {
878
+ if (pth?.nodeName !== 'path') { return false }
879
+ const tlist = pth.transform.baseVal
880
+ const m = transformListToTransform(tlist).matrix
881
+ tlist.clear()
882
+ pth.removeAttribute('transform')
883
+ const segList = pth.pathSegList
884
+
885
+ // Opera/win/non-EN throws an error here.
886
+ // TODO: Find out why!
887
+ // Presumed fixed in Opera 10.5, so commented out for now
888
+
889
+ // try {
890
+ const len = segList.numberOfItems
891
+ // } catch(err) {
892
+ // const fixed_d = pathActions.convertPath(pth);
893
+ // pth.setAttribute('d', fixed_d);
894
+ // segList = pth.pathSegList;
895
+ // const len = segList.numberOfItems;
896
+ // }
897
+ // let lastX, lastY;
898
+ for (let i = 0; i < len; ++i) {
899
+ const seg = segList.getItem(i)
900
+ const type = seg.pathSegType
901
+ if (type === 1) { continue }
902
+ const pts = [];
903
+ ['', 1, 2].forEach(function (n) {
904
+ const x = seg['x' + n]; const y = seg['y' + n]
905
+ if (x !== undefined && y !== undefined) {
906
+ const pt = transformPoint(x, y, m)
907
+ pts.splice(pts.length, 0, pt.x, pt.y)
908
+ }
909
+ })
910
+ svgCanvas.replacePathSeg(type, i, pts, pth)
911
+ }
912
+
913
+ svgCanvas.reorientGrads(pth, m)
914
+ return undefined
915
+ },
916
+ /**
917
+ * @returns {void}
918
+ */
919
+ zoomChange () {
920
+ if (svgCanvas.getCurrentMode() === 'pathedit') {
921
+ path.update()
922
+ }
923
+ },
924
+ /**
925
+ * @typedef {PlainObject} module:path.NodePoint
926
+ * @property {Float} x
927
+ * @property {Float} y
928
+ * @property {Integer} type
929
+ */
930
+ /**
931
+ * @returns {module:path.NodePoint}
932
+ */
933
+ getNodePoint () {
934
+ const selPt = path.selected_pts.length ? path.selected_pts[0] : 1
935
+
936
+ const seg = path.segs[selPt]
937
+ return {
938
+ x: seg.item.x,
939
+ y: seg.item.y,
940
+ type: seg.type
941
+ }
942
+ },
943
+ /**
944
+ * @param {boolean} linkPoints
945
+ * @returns {void}
946
+ */
947
+ linkControlPoints (linkPoints) {
948
+ svgCanvas.setLinkControlPoints(linkPoints)
949
+ },
950
+ /**
951
+ * @returns {void}
952
+ */
953
+ clonePathNode () {
954
+ path.storeD()
955
+
956
+ const selPts = path.selected_pts
957
+ // const {segs} = path;
958
+
959
+ let i = selPts.length
960
+ const nums = []
961
+
962
+ while (i--) {
963
+ const pt = selPts[i]
964
+ path.addSeg(pt)
965
+
966
+ nums.push(pt + i)
967
+ nums.push(pt + i + 1)
968
+ }
969
+ path.init().addPtsToSelection(nums)
970
+
971
+ path.endChanges('Clone path node(s)')
972
+ },
973
+ /**
974
+ * @returns {void}
975
+ */
976
+ opencloseSubPath () {
977
+ const selPts = path.selected_pts
978
+ // Only allow one selected node for now
979
+ if (selPts.length !== 1) { return }
980
+
981
+ const { elem } = path
982
+ const list = elem.pathSegList
983
+
984
+ // const len = list.numberOfItems;
985
+
986
+ const index = selPts[0]
987
+
988
+ let openPt = null
989
+ let startItem = null
990
+
991
+ // Check if subpath is already open
992
+ path.eachSeg(function (i) {
993
+ if (this.type === 2 && i <= index) {
994
+ startItem = this.item
995
+ }
996
+ if (i <= index) { return true }
997
+ if (this.type === 2) {
998
+ // Found M first, so open
999
+ openPt = i
1000
+ return false
1001
+ }
1002
+ if (this.type === 1) {
1003
+ // Found Z first, so closed
1004
+ openPt = false
1005
+ return false
1006
+ }
1007
+ return true
1008
+ })
1009
+
1010
+ if (!openPt) {
1011
+ // Single path, so close last seg
1012
+ openPt = path.segs.length - 1
1013
+ }
1014
+
1015
+ if (openPt !== false) {
1016
+ // Close this path
1017
+
1018
+ // Create a line going to the previous "M"
1019
+ const newseg = elem.createSVGPathSegLinetoAbs(startItem.x, startItem.y)
1020
+
1021
+ const closer = elem.createSVGPathSegClosePath()
1022
+ if (openPt === path.segs.length - 1) {
1023
+ list.appendItem(newseg)
1024
+ list.appendItem(closer)
1025
+ } else {
1026
+ list.insertItemBefore(closer, openPt)
1027
+ list.insertItemBefore(newseg, openPt)
1028
+ }
1029
+
1030
+ path.init().selectPt(openPt + 1)
1031
+ return
1032
+ }
1033
+
1034
+ // M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2
1035
+ // M 2,2 L 3,3 L 1,1
1036
+
1037
+ // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z
1038
+ // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z
1039
+
1040
+ const seg = path.segs[index]
1041
+
1042
+ if (seg.mate) {
1043
+ list.removeItem(index) // Removes last "L"
1044
+ list.removeItem(index) // Removes the "Z"
1045
+ path.init().selectPt(index - 1)
1046
+ return
1047
+ }
1048
+
1049
+ let lastM; let zSeg
1050
+
1051
+ // Find this sub-path's closing point and remove
1052
+ for (let i = 0; i < list.numberOfItems; i++) {
1053
+ const item = list.getItem(i)
1054
+
1055
+ if (item.pathSegType === 2) {
1056
+ // Find the preceding M
1057
+ lastM = i
1058
+ } else if (i === index) {
1059
+ // Remove it
1060
+ list.removeItem(lastM)
1061
+ // index--;
1062
+ } else if (item.pathSegType === 1 && index < i) {
1063
+ // Remove the closing seg of this subpath
1064
+ zSeg = i - 1
1065
+ list.removeItem(i)
1066
+ break
1067
+ }
1068
+ }
1069
+
1070
+ let num = (index - lastM) - 1
1071
+
1072
+ while (num--) {
1073
+ list.insertItemBefore(list.getItem(lastM), zSeg)
1074
+ }
1075
+
1076
+ const pt = list.getItem(lastM)
1077
+
1078
+ // Make this point the new "M"
1079
+ svgCanvas.replacePathSeg(2, lastM, [pt.x, pt.y])
1080
+
1081
+ // i = index; // i is local here, so has no effect; what was the intent for this?
1082
+
1083
+ path.init().selectPt(0)
1084
+ },
1085
+ /**
1086
+ * @returns {void}
1087
+ */
1088
+ deletePathNode () {
1089
+ if (!pathActionsMethod.canDeleteNodes) { return }
1090
+ path.storeD()
1091
+
1092
+ const selPts = path.selected_pts
1093
+
1094
+ let i = selPts.length
1095
+ while (i--) {
1096
+ const pt = selPts[i]
1097
+ path.deleteSeg(pt)
1098
+ }
1099
+
1100
+ // Cleanup
1101
+ const cleanup = function () {
1102
+ const segList = path.elem.pathSegList
1103
+ let len = segList.numberOfItems
1104
+
1105
+ const remItems = function (pos, count) {
1106
+ while (count--) {
1107
+ segList.removeItem(pos)
1108
+ }
1109
+ }
1110
+
1111
+ if (len <= 1) { return true }
1112
+
1113
+ while (len--) {
1114
+ const item = segList.getItem(len)
1115
+ if (item.pathSegType === 1) {
1116
+ const prev = segList.getItem(len - 1)
1117
+ const nprev = segList.getItem(len - 2)
1118
+ if (prev.pathSegType === 2) {
1119
+ remItems(len - 1, 2)
1120
+ cleanup()
1121
+ break
1122
+ } else if (nprev.pathSegType === 2) {
1123
+ remItems(len - 2, 3)
1124
+ cleanup()
1125
+ break
1126
+ }
1127
+ } else if (item.pathSegType === 2 && len > 0) {
1128
+ const prevType = segList.getItem(len - 1).pathSegType
1129
+ // Path has M M
1130
+ if (prevType === 2) {
1131
+ remItems(len - 1, 1)
1132
+ cleanup()
1133
+ break
1134
+ // Entire path ends with Z M
1135
+ } else if (prevType === 1 && segList.numberOfItems - 1 === len) {
1136
+ remItems(len, 1)
1137
+ cleanup()
1138
+ break
1139
+ }
1140
+ }
1141
+ }
1142
+ return false
1143
+ }
1144
+
1145
+ cleanup()
1146
+
1147
+ // Completely delete a path with 1 or 0 segments
1148
+ if (path.elem.pathSegList.numberOfItems <= 1) {
1149
+ pathActionsMethod.toSelectMode(path.elem)
1150
+ svgCanvas.canvas.deleteSelectedElements()
1151
+ return
1152
+ }
1153
+
1154
+ path.init()
1155
+ path.clearSelection()
1156
+
1157
+ // TODO: Find right way to select point now
1158
+ // path.selectPt(selPt);
1159
+ if (window.opera) { // Opera repaints incorrectly
1160
+ path.elem.setAttribute('d', path.elem.getAttribute('d'))
1161
+ }
1162
+ path.endChanges('Delete path node(s)')
1163
+ },
1164
+ // Can't seem to use `@borrows` here, so using `@see`
1165
+ /**
1166
+ * Smooth polyline into path.
1167
+ * @function module:path.pathActions.smoothPolylineIntoPath
1168
+ * @see module:path~smoothPolylineIntoPath
1169
+ */
1170
+ smoothPolylineIntoPath,
1171
+ /* eslint-enable */
1172
+ /**
1173
+ * @param {?Integer} v See {@link https://www.w3.org/TR/SVG/single-page.html#paths-InterfaceSVGPathSeg}
1174
+ * @returns {void}
1175
+ */
1176
+ setSegType (v) {
1177
+ path?.setSegType(v)
1178
+ },
1179
+ /**
1180
+ * @param {string} attr
1181
+ * @param {Float} newValue
1182
+ * @returns {void}
1183
+ */
1184
+ moveNode (attr, newValue) {
1185
+ const selPts = path.selected_pts
1186
+ if (!selPts.length) { return }
1187
+
1188
+ path.storeD()
1189
+
1190
+ // Get first selected point
1191
+ const seg = path.segs[selPts[0]]
1192
+ const diff = { x: 0, y: 0 }
1193
+ diff[attr] = newValue - seg.item[attr]
1194
+
1195
+ seg.move(diff.x, diff.y)
1196
+ path.endChanges('Move path point')
1197
+ },
1198
+ /**
1199
+ * @param {Element} elem
1200
+ * @returns {void}
1201
+ */
1202
+ fixEnd (elem) {
1203
+ // Adds an extra segment if the last seg before a Z doesn't end
1204
+ // at its M point
1205
+ // M0,0 L0,100 L100,100 z
1206
+ const segList = elem.pathSegList
1207
+ const len = segList.numberOfItems
1208
+ let lastM
1209
+ for (let i = 0; i < len; ++i) {
1210
+ const item = segList.getItem(i)
1211
+ if (item.pathSegType === 2) { // 2 => M segment type (move to)
1212
+ lastM = item
1213
+ }
1214
+
1215
+ if (item.pathSegType === 1) { // 1 => Z segment type (close path)
1216
+ const prev = segList.getItem(i - 1)
1217
+ if (prev.x !== lastM.x || prev.y !== lastM.y) {
1218
+ // Add an L segment here
1219
+ const newseg = elem.createSVGPathSegLinetoAbs(lastM.x, lastM.y)
1220
+ segList.insertItemBefore(newseg, i)
1221
+ // Can this be done better?
1222
+ pathActionsMethod.fixEnd(elem)
1223
+ break
1224
+ }
1225
+ }
1226
+ }
1227
+ },
1228
+ // Can't seem to use `@borrows` here, so using `@see`
1229
+ /**
1230
+ * Convert a path to one with only absolute or relative values.
1231
+ * @function module:path.pathActions.convertPath
1232
+ * @see module:path.convertPath
1233
+ */
1234
+ convertPath
1235
+ })
1236
+ })()
1237
+ // end pathActions