@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-method.js ADDED
@@ -0,0 +1,1012 @@
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 { ChangeElementCommand } from './history.js'
11
+ import {
12
+ transformPoint, getMatrix
13
+ } from './math.js'
14
+ import {
15
+ assignAttributes, getRotationAngle,
16
+ getElement
17
+ } from './utilities.js'
18
+
19
+ let svgCanvas = null
20
+
21
+ /**
22
+ * @function module:path-actions.init
23
+ * @param {module:path-actions.svgCanvas} pathMethodsContext
24
+ * @returns {void}
25
+ */
26
+ export const init = (canvas) => {
27
+ svgCanvas = canvas
28
+ }
29
+
30
+ /* eslint-disable max-len */
31
+ /**
32
+ * @function module:path.ptObjToArr
33
+ * @todo See if this should just live in `replacePathSeg`
34
+ * @param {string} type
35
+ * @param {SVGPathSegMovetoAbs|SVGPathSegLinetoAbs|SVGPathSegCurvetoCubicAbs|SVGPathSegCurvetoQuadraticAbs|SVGPathSegArcAbs|SVGPathSegLinetoHorizontalAbs|SVGPathSegLinetoVerticalAbs|SVGPathSegCurvetoCubicSmoothAbs|SVGPathSegCurvetoQuadraticSmoothAbs} segItem
36
+ * @returns {ArgumentsArray}
37
+ */
38
+ /* eslint-enable max-len */
39
+ export const ptObjToArrMethod = function (type, segItem) {
40
+ const segData = svgCanvas.getSegData()
41
+ const props = segData[type]
42
+ return props.map((prop) => {
43
+ return segItem[prop]
44
+ })
45
+ }
46
+
47
+ /**
48
+ * @function module:path.getGripPt
49
+ * @param {Segment} seg
50
+ * @param {module:math.XYObject} altPt
51
+ * @returns {module:math.XYObject}
52
+ */
53
+ export const getGripPtMethod = function (seg, altPt) {
54
+ const { path: pth } = seg
55
+ let out = {
56
+ x: altPt ? altPt.x : seg.item.x,
57
+ y: altPt ? altPt.y : seg.item.y
58
+ }
59
+
60
+ if (pth.matrix) {
61
+ const pt = transformPoint(out.x, out.y, pth.matrix)
62
+ out = pt
63
+ }
64
+ const zoom = svgCanvas.getZoom()
65
+ out.x *= zoom
66
+ out.y *= zoom
67
+
68
+ return out
69
+ }
70
+ /**
71
+ * @function module:path.getPointFromGrip
72
+ * @param {module:math.XYObject} pt
73
+ * @param {module:path.Path} pth
74
+ * @returns {module:math.XYObject}
75
+ */
76
+ export const getPointFromGripMethod = function (pt, pth) {
77
+ const out = {
78
+ x: pt.x,
79
+ y: pt.y
80
+ }
81
+
82
+ if (pth.matrix) {
83
+ pt = transformPoint(out.x, out.y, pth.imatrix)
84
+ out.x = pt.x
85
+ out.y = pt.y
86
+ }
87
+ const zoom = svgCanvas.getZoom()
88
+ out.x /= zoom
89
+ out.y /= zoom
90
+
91
+ return out
92
+ }
93
+ /**
94
+ * @function module:path.getGripContainer
95
+ * @returns {Element}
96
+ */
97
+ export const getGripContainerMethod = function () {
98
+ let c = getElement('pathpointgrip_container')
99
+ if (!c) {
100
+ const parentElement = getElement('selectorParentGroup')
101
+ c = document.createElementNS(NS.SVG, 'g')
102
+ parentElement.append(c)
103
+ c.id = 'pathpointgrip_container'
104
+ }
105
+ return c
106
+ }
107
+ /**
108
+ * Requires prior call to `setUiStrings` if `xlink:title`
109
+ * to be set on the grip.
110
+ * @function module:path.addPointGrip
111
+ * @param {Integer} index
112
+ * @param {Integer} x
113
+ * @param {Integer} y
114
+ * @returns {SVGCircleElement}
115
+ */
116
+ export const addPointGripMethod = function (index, x, y) {
117
+ // create the container of all the point grips
118
+ const pointGripContainer = getGripContainerMethod()
119
+
120
+ let pointGrip = getElement('pathpointgrip_' + index)
121
+ // create it
122
+ if (!pointGrip) {
123
+ pointGrip = document.createElementNS(NS.SVG, 'circle')
124
+ const atts = {
125
+ id: 'pathpointgrip_' + index,
126
+ display: 'none',
127
+ r: 4,
128
+ fill: '#0FF',
129
+ stroke: '#00F',
130
+ 'stroke-width': 2,
131
+ cursor: 'move',
132
+ style: 'pointer-events:all'
133
+ }
134
+ const uiStrings = svgCanvas.getUIStrings()
135
+ if ('pathNodeTooltip' in uiStrings) { // May be empty if running path.js without svg-editor
136
+ atts['xlink:title'] = uiStrings.pathNodeTooltip
137
+ }
138
+ assignAttributes(pointGrip, atts)
139
+ pointGripContainer.append(pointGrip)
140
+
141
+ const grip = document.getElementById('pathpointgrip_' + index)
142
+ grip?.addEventListener('dblclick', () => {
143
+ const path = svgCanvas.getPathObj()
144
+ if (path) {
145
+ path.setSegType()
146
+ }
147
+ })
148
+ }
149
+ if (x && y) {
150
+ // set up the point grip element and display it
151
+ assignAttributes(pointGrip, {
152
+ cx: x,
153
+ cy: y,
154
+ display: 'inline'
155
+ })
156
+ }
157
+ return pointGrip
158
+ }
159
+ /**
160
+ * Requires prior call to `setUiStrings` if `xlink:title`
161
+ * to be set on the grip.
162
+ * @function module:path.addCtrlGrip
163
+ * @param {string} id
164
+ * @returns {SVGCircleElement}
165
+ */
166
+ export const addCtrlGripMethod = function (id) {
167
+ let pointGrip = getElement('ctrlpointgrip_' + id)
168
+ if (pointGrip) { return pointGrip }
169
+
170
+ pointGrip = document.createElementNS(NS.SVG, 'circle')
171
+ const atts = {
172
+ id: 'ctrlpointgrip_' + id,
173
+ display: 'none',
174
+ r: 4,
175
+ fill: '#0FF',
176
+ stroke: '#55F',
177
+ 'stroke-width': 1,
178
+ cursor: 'move',
179
+ style: 'pointer-events:all'
180
+ }
181
+ const uiStrings = svgCanvas.getUIStrings()
182
+ if ('pathCtrlPtTooltip' in uiStrings) { // May be empty if running path.js without svg-editor
183
+ atts['xlink:title'] = uiStrings.pathCtrlPtTooltip
184
+ }
185
+ assignAttributes(pointGrip, atts)
186
+ getGripContainerMethod().append(pointGrip)
187
+ return pointGrip
188
+ }
189
+ /**
190
+ * @function module:path.getCtrlLine
191
+ * @param {string} id
192
+ * @returns {SVGLineElement}
193
+ */
194
+ export const getCtrlLineMethod = function (id) {
195
+ let ctrlLine = getElement('ctrlLine_' + id)
196
+ if (ctrlLine) { return ctrlLine }
197
+
198
+ ctrlLine = document.createElementNS(NS.SVG, 'line')
199
+ assignAttributes(ctrlLine, {
200
+ id: 'ctrlLine_' + id,
201
+ stroke: '#555',
202
+ 'stroke-width': 1,
203
+ style: 'pointer-events:none'
204
+ })
205
+ getGripContainerMethod().append(ctrlLine)
206
+ return ctrlLine
207
+ }
208
+ /**
209
+ * @function module:path.getPointGrip
210
+ * @param {Segment} seg
211
+ * @param {boolean} update
212
+ * @returns {SVGCircleElement}
213
+ */
214
+ export const getPointGripMethod = function (seg, update) {
215
+ const { index } = seg
216
+ const pointGrip = addPointGripMethod(index)
217
+
218
+ if (update) {
219
+ const pt = getGripPtMethod(seg)
220
+ assignAttributes(pointGrip, {
221
+ cx: pt.x,
222
+ cy: pt.y,
223
+ display: 'inline'
224
+ })
225
+ }
226
+
227
+ return pointGrip
228
+ }
229
+ /**
230
+ * @function module:path.getControlPoints
231
+ * @param {Segment} seg
232
+ * @returns {PlainObject<string, SVGLineElement|SVGCircleElement>}
233
+ */
234
+ export const getControlPointsMethod = function (seg) {
235
+ const { item, index } = seg
236
+ if (!('x1' in item) || !('x2' in item)) { return null }
237
+ const cpt = {}
238
+ /* const pointGripContainer = */ getGripContainerMethod()
239
+
240
+ // Note that this is intentionally not seg.prev.item
241
+ const path = svgCanvas.getPathObj()
242
+ const prev = path.segs[index - 1].item
243
+
244
+ const segItems = [prev, item]
245
+
246
+ for (let i = 1; i < 3; i++) {
247
+ const id = index + 'c' + i
248
+
249
+ const ctrlLine = cpt['c' + i + '_line'] = getCtrlLineMethod(id)
250
+
251
+ const pt = getGripPtMethod(seg, { x: item['x' + i], y: item['y' + i] })
252
+ const gpt = getGripPtMethod(seg, { x: segItems[i - 1].x, y: segItems[i - 1].y })
253
+
254
+ assignAttributes(ctrlLine, {
255
+ x1: pt.x,
256
+ y1: pt.y,
257
+ x2: gpt.x,
258
+ y2: gpt.y,
259
+ display: 'inline'
260
+ })
261
+
262
+ cpt['c' + i + '_line'] = ctrlLine
263
+
264
+ // create it
265
+ const pointGrip = cpt['c' + i] = addCtrlGripMethod(id)
266
+
267
+ assignAttributes(pointGrip, {
268
+ cx: pt.x,
269
+ cy: pt.y,
270
+ display: 'inline'
271
+ })
272
+ cpt['c' + i] = pointGrip
273
+ }
274
+ return cpt
275
+ }
276
+ /**
277
+ * This replaces the segment at the given index. Type is given as number.
278
+ * @function module:path.replacePathSeg
279
+ * @param {Integer} type Possible values set during {@link module:path.init}
280
+ * @param {Integer} index
281
+ * @param {ArgumentsArray} pts
282
+ * @param {SVGPathElement} elem
283
+ * @returns {void}
284
+ */
285
+ export const replacePathSegMethod = function (type, index, pts, elem) {
286
+ const path = svgCanvas.getPathObj()
287
+ const pth = elem || path.elem
288
+ const pathFuncs = svgCanvas.getPathFuncs()
289
+ const func = 'createSVGPathSeg' + pathFuncs[type]
290
+ const seg = pth[func](...pts)
291
+
292
+ pth.pathSegList.replaceItem(seg, index)
293
+ }
294
+ /**
295
+ * @function module:path.getSegSelector
296
+ * @param {Segment} seg
297
+ * @param {boolean} update
298
+ * @returns {SVGPathElement}
299
+ */
300
+ export const getSegSelectorMethod = function (seg, update) {
301
+ const { index } = seg
302
+ let segLine = getElement('segline_' + index)
303
+ if (!segLine) {
304
+ const pointGripContainer = getGripContainerMethod()
305
+ // create segline
306
+ segLine = document.createElementNS(NS.SVG, 'path')
307
+ assignAttributes(segLine, {
308
+ id: 'segline_' + index,
309
+ display: 'none',
310
+ fill: 'none',
311
+ stroke: '#0FF',
312
+ 'stroke-width': 2,
313
+ style: 'pointer-events:none',
314
+ d: 'M0,0 0,0'
315
+ })
316
+ pointGripContainer.append(segLine)
317
+ }
318
+
319
+ if (update) {
320
+ const { prev } = seg
321
+ if (!prev) {
322
+ segLine.setAttribute('display', 'none')
323
+ return segLine
324
+ }
325
+
326
+ const pt = getGripPtMethod(prev)
327
+ // Set start point
328
+ replacePathSegMethod(2, 0, [pt.x, pt.y], segLine)
329
+
330
+ const pts = ptObjToArrMethod(seg.type, seg.item) // , true);
331
+ for (let i = 0; i < pts.length; i += 2) {
332
+ const point = getGripPtMethod(seg, { x: pts[i], y: pts[i + 1] })
333
+ pts[i] = point.x
334
+ pts[i + 1] = point.y
335
+ }
336
+
337
+ replacePathSegMethod(seg.type, 1, pts, segLine)
338
+ }
339
+ return segLine
340
+ }
341
+ /**
342
+ *
343
+ */
344
+ export class Segment {
345
+ /**
346
+ * @param {Integer} index
347
+ * @param {SVGPathSeg} item
348
+ * @todo Is `item` be more constrained here?
349
+ */
350
+ constructor (index, item) {
351
+ this.selected = false
352
+ this.index = index
353
+ this.item = item
354
+ this.type = item.pathSegType
355
+
356
+ this.ctrlpts = []
357
+ this.ptgrip = null
358
+ this.segsel = null
359
+ }
360
+
361
+ /**
362
+ * @param {boolean} y
363
+ * @returns {void}
364
+ */
365
+ showCtrlPts (y) {
366
+ for (const i in this.ctrlpts) {
367
+ if ({}.hasOwnProperty.call(this.ctrlpts, i)) {
368
+ this.ctrlpts[i].setAttribute('display', y ? 'inline' : 'none')
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * @param {boolean} y
375
+ * @returns {void}
376
+ */
377
+ selectCtrls (y) {
378
+ document.getElementById('ctrlpointgrip_' + this.index + 'c1').setAttribute('fill', y ? '#0FF' : '#EEE')
379
+ document.getElementById('ctrlpointgrip_' + this.index + 'c2').setAttribute('fill', y ? '#0FF' : '#EEE')
380
+ }
381
+
382
+ /**
383
+ * @param {boolean} y
384
+ * @returns {void}
385
+ */
386
+ show (y) {
387
+ if (this.ptgrip) {
388
+ this.ptgrip.setAttribute('display', y ? 'inline' : 'none')
389
+ this.segsel.setAttribute('display', y ? 'inline' : 'none')
390
+ // Show/hide all control points if available
391
+ this.showCtrlPts(y)
392
+ }
393
+ }
394
+
395
+ /**
396
+ * @param {boolean} y
397
+ * @returns {void}
398
+ */
399
+ select (y) {
400
+ if (this.ptgrip) {
401
+ this.ptgrip.setAttribute('stroke', y ? '#0FF' : '#00F')
402
+ this.segsel.setAttribute('display', y ? 'inline' : 'none')
403
+ if (this.ctrlpts) {
404
+ this.selectCtrls(y)
405
+ }
406
+ this.selected = y
407
+ }
408
+ }
409
+
410
+ /**
411
+ * @returns {void}
412
+ */
413
+ addGrip () {
414
+ this.ptgrip = getPointGripMethod(this, true)
415
+ this.ctrlpts = getControlPointsMethod(this) // , true);
416
+ this.segsel = getSegSelectorMethod(this, true)
417
+ }
418
+
419
+ /**
420
+ * @param {boolean} full
421
+ * @returns {void}
422
+ */
423
+ update (full) {
424
+ if (this.ptgrip) {
425
+ const pt = getGripPtMethod(this)
426
+ assignAttributes(this.ptgrip, {
427
+ cx: pt.x,
428
+ cy: pt.y
429
+ })
430
+
431
+ getSegSelectorMethod(this, true)
432
+
433
+ if (this.ctrlpts) {
434
+ if (full) {
435
+ const path = svgCanvas.getPathObj()
436
+ this.item = path.elem.pathSegList.getItem(this.index)
437
+ this.type = this.item.pathSegType
438
+ }
439
+ getControlPointsMethod(this)
440
+ }
441
+ // this.segsel.setAttribute('display', y ? 'inline' : 'none');
442
+ }
443
+ }
444
+
445
+ /**
446
+ * @param {Integer} dx
447
+ * @param {Integer} dy
448
+ * @returns {void}
449
+ */
450
+ move (dx, dy) {
451
+ const { item } = this
452
+
453
+ const curPts = this.ctrlpts
454
+ ? [
455
+ item.x += dx, item.y += dy,
456
+ item.x1, item.y1, item.x2 += dx, item.y2 += dy
457
+ ]
458
+ : [item.x += dx, item.y += dy]
459
+
460
+ replacePathSegMethod(
461
+ this.type,
462
+ this.index,
463
+ // type 10 means ARC
464
+ this.type === 10 ? ptObjToArrMethod(this.type, item) : curPts
465
+ )
466
+
467
+ if (this.next?.ctrlpts) {
468
+ const next = this.next.item
469
+ const nextPts = [
470
+ next.x, next.y,
471
+ next.x1 += dx, next.y1 += dy, next.x2, next.y2
472
+ ]
473
+ replacePathSegMethod(this.next.type, this.next.index, nextPts)
474
+ }
475
+
476
+ if (this.mate) {
477
+ // The last point of a closed subpath has a 'mate',
478
+ // which is the 'M' segment of the subpath
479
+ const { item: itm } = this.mate
480
+ const pts = [itm.x += dx, itm.y += dy]
481
+ replacePathSegMethod(this.mate.type, this.mate.index, pts)
482
+ // Has no grip, so does not need 'updating'?
483
+ }
484
+
485
+ this.update(true)
486
+ if (this.next) { this.next.update(true) }
487
+ }
488
+
489
+ /**
490
+ * @param {Integer} num
491
+ * @returns {void}
492
+ */
493
+ setLinked (num) {
494
+ let seg; let anum; let pt
495
+ if (num === 2) {
496
+ anum = 1
497
+ seg = this.next
498
+ if (!seg) { return }
499
+ pt = this.item
500
+ } else {
501
+ anum = 2
502
+ seg = this.prev
503
+ if (!seg) { return }
504
+ pt = seg.item
505
+ }
506
+
507
+ const { item } = seg
508
+ item['x' + anum] = pt.x + (pt.x - this.item['x' + num])
509
+ item['y' + anum] = pt.y + (pt.y - this.item['y' + num])
510
+
511
+ const pts = [
512
+ item.x, item.y,
513
+ item.x1, item.y1,
514
+ item.x2, item.y2
515
+ ]
516
+
517
+ replacePathSegMethod(seg.type, seg.index, pts)
518
+ seg.update(true)
519
+ }
520
+
521
+ /**
522
+ * @param {Integer} num
523
+ * @param {Integer} dx
524
+ * @param {Integer} dy
525
+ * @returns {void}
526
+ */
527
+ moveCtrl (num, dx, dy) {
528
+ const { item } = this
529
+ item['x' + num] += dx
530
+ item['y' + num] += dy
531
+
532
+ const pts = [
533
+ item.x, item.y,
534
+ item.x1, item.y1, item.x2, item.y2
535
+ ]
536
+
537
+ replacePathSegMethod(this.type, this.index, pts)
538
+ this.update(true)
539
+ }
540
+
541
+ /**
542
+ * @param {Integer} newType Possible values set during {@link module:path.init}
543
+ * @param {ArgumentsArray} pts
544
+ * @returns {void}
545
+ */
546
+ setType (newType, pts) {
547
+ replacePathSegMethod(newType, this.index, pts)
548
+ this.type = newType
549
+ const path = svgCanvas.getPathObj()
550
+ this.item = path.elem.pathSegList.getItem(this.index)
551
+ this.showCtrlPts(newType === 6)
552
+ this.ctrlpts = getControlPointsMethod(this)
553
+ this.update(true)
554
+ }
555
+ }
556
+
557
+ /**
558
+ *
559
+ */
560
+ export class Path {
561
+ /**
562
+ * @param {SVGPathElement} elem
563
+ * @throws {Error} If constructed without a path element
564
+ */
565
+ constructor (elem) {
566
+ if (!elem || elem.tagName !== 'path') {
567
+ throw new Error('svgedit.path.Path constructed without a <path> element')
568
+ }
569
+
570
+ this.elem = elem
571
+ this.segs = []
572
+ this.selected_pts = []
573
+ svgCanvas.setPathObj(this)
574
+ // path = this;
575
+
576
+ this.init()
577
+ }
578
+
579
+ setPathContext () {
580
+ svgCanvas.setPathObj(this)
581
+ }
582
+
583
+ /**
584
+ * Reset path data.
585
+ * @returns {module:path.Path}
586
+ */
587
+ init () {
588
+ // Hide all grips, etc
589
+
590
+ // fixed, needed to work on all found elements, not just first
591
+ const pointGripContainer = getGripContainerMethod()
592
+ const elements = pointGripContainer.querySelectorAll('*')
593
+ Array.prototype.forEach.call(elements, function (el) {
594
+ el.setAttribute('display', 'none')
595
+ })
596
+
597
+ const segList = this.elem.pathSegList
598
+ const len = segList.numberOfItems
599
+ this.segs = []
600
+ this.selected_pts = []
601
+ this.first_seg = null
602
+
603
+ // Set up segs array
604
+ for (let i = 0; i < len; i++) {
605
+ const item = segList.getItem(i)
606
+ const segment = new Segment(i, item)
607
+ segment.path = this
608
+ this.segs.push(segment)
609
+ }
610
+
611
+ const { segs } = this
612
+
613
+ let startI = null
614
+ for (let i = 0; i < len; i++) {
615
+ const seg = segs[i]
616
+ const nextSeg = (i + 1) >= len ? null : segs[i + 1]
617
+ const prevSeg = (i - 1) < 0 ? null : segs[i - 1]
618
+ if (seg.type === 2) {
619
+ if (prevSeg && prevSeg.type !== 1) {
620
+ // New sub-path, last one is open,
621
+ // so add a grip to last sub-path's first point
622
+ const startSeg = segs[startI]
623
+ startSeg.next = segs[startI + 1]
624
+ startSeg.next.prev = startSeg
625
+ startSeg.addGrip()
626
+ }
627
+ // Remember that this is a starter seg
628
+ startI = i
629
+ } else if (nextSeg?.type === 1) {
630
+ // This is the last real segment of a closed sub-path
631
+ // Next is first seg after "M"
632
+ seg.next = segs[startI + 1]
633
+
634
+ // First seg after "M"'s prev is this
635
+ seg.next.prev = seg
636
+ seg.mate = segs[startI]
637
+ seg.addGrip()
638
+ if (!this.first_seg) {
639
+ this.first_seg = seg
640
+ }
641
+ } else if (!nextSeg) {
642
+ if (seg.type !== 1) {
643
+ // Last seg, doesn't close so add a grip
644
+ // to last sub-path's first point
645
+ const startSeg = segs[startI]
646
+ startSeg.next = segs[startI + 1]
647
+ startSeg.next.prev = startSeg
648
+ startSeg.addGrip()
649
+ seg.addGrip()
650
+
651
+ if (!this.first_seg) {
652
+ // Open path, so set first as real first and add grip
653
+ this.first_seg = segs[startI]
654
+ }
655
+ }
656
+ } else if (seg.type !== 1) {
657
+ // Regular segment, so add grip and its "next"
658
+ seg.addGrip()
659
+
660
+ // Don't set its "next" if it's an "M"
661
+ if (nextSeg && nextSeg.type !== 2) {
662
+ seg.next = nextSeg
663
+ seg.next.prev = seg
664
+ }
665
+ }
666
+ }
667
+ return this
668
+ }
669
+
670
+ /**
671
+ * @callback module:path.PathEachSegCallback
672
+ * @this module:path.Segment
673
+ * @param {Integer} i The index of the seg being iterated
674
+ * @returns {boolean|void} Will stop execution of `eachSeg` if returns `false`
675
+ */
676
+ /**
677
+ * @param {module:path.PathEachSegCallback} fn
678
+ * @returns {void}
679
+ */
680
+ eachSeg (fn) {
681
+ const len = this.segs.length
682
+ for (let i = 0; i < len; i++) {
683
+ const ret = fn.call(this.segs[i], i)
684
+ if (ret === false) { break }
685
+ }
686
+ }
687
+
688
+ /**
689
+ * @param {Integer} index
690
+ * @returns {void}
691
+ */
692
+ addSeg (index) {
693
+ // Adds a new segment
694
+ const seg = this.segs[index]
695
+ if (!seg.prev) { return }
696
+
697
+ const { prev } = seg
698
+ let newseg; let newX; let newY
699
+ switch (seg.item.pathSegType) {
700
+ case 4: {
701
+ newX = (seg.item.x + prev.item.x) / 2
702
+ newY = (seg.item.y + prev.item.y) / 2
703
+ newseg = this.elem.createSVGPathSegLinetoAbs(newX, newY)
704
+ break
705
+ } case 6: { // make it a curved segment to preserve the shape (WRS)
706
+ // https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm#Geometric_interpretation
707
+ const p0x = (prev.item.x + seg.item.x1) / 2
708
+ const p1x = (seg.item.x1 + seg.item.x2) / 2
709
+ const p2x = (seg.item.x2 + seg.item.x) / 2
710
+ const p01x = (p0x + p1x) / 2
711
+ const p12x = (p1x + p2x) / 2
712
+ newX = (p01x + p12x) / 2
713
+ const p0y = (prev.item.y + seg.item.y1) / 2
714
+ const p1y = (seg.item.y1 + seg.item.y2) / 2
715
+ const p2y = (seg.item.y2 + seg.item.y) / 2
716
+ const p01y = (p0y + p1y) / 2
717
+ const p12y = (p1y + p2y) / 2
718
+ newY = (p01y + p12y) / 2
719
+ newseg = this.elem.createSVGPathSegCurvetoCubicAbs(newX, newY, p0x, p0y, p01x, p01y)
720
+ const pts = [seg.item.x, seg.item.y, p12x, p12y, p2x, p2y]
721
+ replacePathSegMethod(seg.type, index, pts)
722
+ break
723
+ }
724
+ }
725
+ const list = this.elem.pathSegList
726
+ list.insertItemBefore(newseg, index)
727
+ }
728
+
729
+ /**
730
+ * @param {Integer} index
731
+ * @returns {void}
732
+ */
733
+ deleteSeg (index) {
734
+ const seg = this.segs[index]
735
+ const list = this.elem.pathSegList
736
+
737
+ seg.show(false)
738
+ const { next } = seg
739
+ if (seg.mate) {
740
+ // Make the next point be the "M" point
741
+ const pt = [next.item.x, next.item.y]
742
+ replacePathSegMethod(2, next.index, pt)
743
+
744
+ // Reposition last node
745
+ replacePathSegMethod(4, seg.index, pt)
746
+
747
+ list.removeItem(seg.mate.index)
748
+ } else if (!seg.prev) {
749
+ // First node of open path, make next point the M
750
+ // const {item} = seg;
751
+ const pt = [next.item.x, next.item.y]
752
+ replacePathSegMethod(2, seg.next.index, pt)
753
+ list.removeItem(index)
754
+ } else {
755
+ list.removeItem(index)
756
+ }
757
+ }
758
+
759
+ /**
760
+ * @param {Integer} index
761
+ * @returns {void}
762
+ */
763
+ removePtFromSelection (index) {
764
+ const pos = this.selected_pts.indexOf(index)
765
+ if (pos === -1) {
766
+ return
767
+ }
768
+ this.segs[index].select(false)
769
+ this.selected_pts.splice(pos, 1)
770
+ }
771
+
772
+ /**
773
+ * @returns {void}
774
+ */
775
+ clearSelection () {
776
+ this.eachSeg(function () {
777
+ // 'this' is the segment here
778
+ this.select(false)
779
+ })
780
+ this.selected_pts = []
781
+ }
782
+
783
+ /**
784
+ * @returns {void}
785
+ */
786
+ storeD () {
787
+ this.last_d = this.elem.getAttribute('d')
788
+ }
789
+
790
+ /**
791
+ * @param {Integer} y
792
+ * @returns {Path}
793
+ */
794
+ show (y) {
795
+ // Shows this path's segment grips
796
+ this.eachSeg(function () {
797
+ // 'this' is the segment here
798
+ this.show(y)
799
+ })
800
+ if (y) {
801
+ this.selectPt(this.first_seg.index)
802
+ }
803
+ return this
804
+ }
805
+
806
+ /**
807
+ * Move selected points.
808
+ * @param {Integer} dx
809
+ * @param {Integer} dy
810
+ * @returns {void}
811
+ */
812
+ movePts (dx, dy) {
813
+ let i = this.selected_pts.length
814
+ while (i--) {
815
+ const seg = this.segs[this.selected_pts[i]]
816
+ seg.move(dx, dy)
817
+ }
818
+ }
819
+
820
+ /**
821
+ * @param {Integer} dx
822
+ * @param {Integer} dy
823
+ * @returns {void}
824
+ */
825
+ moveCtrl (dx, dy) {
826
+ const seg = this.segs[this.selected_pts[0]]
827
+ seg.moveCtrl(this.dragctrl, dx, dy)
828
+ if (svgCanvas.getLinkControlPts()) {
829
+ seg.setLinked(this.dragctrl)
830
+ }
831
+ }
832
+
833
+ /**
834
+ * @param {?Integer} newType See {@link https://www.w3.org/TR/SVG/single-page.html#paths-InterfaceSVGPathSeg}
835
+ * @returns {void}
836
+ */
837
+ setSegType (newType) {
838
+ this.storeD()
839
+ let i = this.selected_pts.length
840
+ let text
841
+ while (i--) {
842
+ const selPt = this.selected_pts[i]
843
+
844
+ // Selected seg
845
+ const cur = this.segs[selPt]
846
+ const { prev } = cur
847
+ if (!prev) { continue }
848
+
849
+ if (!newType) { // double-click, so just toggle
850
+ text = 'Toggle Path Segment Type'
851
+
852
+ // Toggle segment to curve/straight line
853
+ const oldType = cur.type
854
+
855
+ newType = (oldType === 6) ? 4 : 6
856
+ }
857
+
858
+ newType = Number(newType)
859
+
860
+ const curX = cur.item.x
861
+ const curY = cur.item.y
862
+ const prevX = prev.item.x
863
+ const prevY = prev.item.y
864
+ let points
865
+ switch (newType) {
866
+ case 6: {
867
+ if (cur.olditem) {
868
+ const old = cur.olditem
869
+ points = [curX, curY, old.x1, old.y1, old.x2, old.y2]
870
+ } else {
871
+ const diffX = curX - prevX
872
+ const diffY = curY - prevY
873
+ // get control points from straight line segment
874
+ /*
875
+ const ct1x = (prevX + (diffY/2));
876
+ const ct1y = (prevY - (diffX/2));
877
+ const ct2x = (curX + (diffY/2));
878
+ const ct2y = (curY - (diffX/2));
879
+ */
880
+ // create control points on the line to preserve the shape (WRS)
881
+ const ct1x = (prevX + (diffX / 3))
882
+ const ct1y = (prevY + (diffY / 3))
883
+ const ct2x = (curX - (diffX / 3))
884
+ const ct2y = (curY - (diffY / 3))
885
+ points = [curX, curY, ct1x, ct1y, ct2x, ct2y]
886
+ }
887
+ break
888
+ } case 4: {
889
+ points = [curX, curY]
890
+
891
+ // Store original prevve segment nums
892
+ cur.olditem = cur.item
893
+ break
894
+ }
895
+ }
896
+
897
+ cur.setType(newType, points)
898
+ }
899
+ const path = svgCanvas.getPathObj()
900
+ path.endChanges(text)
901
+ }
902
+
903
+ /**
904
+ * @param {Integer} pt
905
+ * @param {Integer} ctrlNum
906
+ * @returns {void}
907
+ */
908
+ selectPt (pt, ctrlNum) {
909
+ this.clearSelection()
910
+ if (!pt) {
911
+ this.eachSeg(function (i) {
912
+ // 'this' is the segment here.
913
+ if (this.prev) {
914
+ pt = i
915
+ }
916
+ })
917
+ }
918
+ this.addPtsToSelection(pt)
919
+ if (ctrlNum) {
920
+ this.dragctrl = ctrlNum
921
+
922
+ if (svgCanvas.getLinkControlPts()) {
923
+ this.segs[pt].setLinked(ctrlNum)
924
+ }
925
+ }
926
+ }
927
+
928
+ /**
929
+ * Update position of all points.
930
+ * @returns {Path}
931
+ */
932
+ update () {
933
+ const { elem } = this
934
+ if (getRotationAngle(elem)) {
935
+ this.matrix = getMatrix(elem)
936
+ this.imatrix = this.matrix.inverse()
937
+ } else {
938
+ this.matrix = null
939
+ this.imatrix = null
940
+ }
941
+
942
+ this.eachSeg(function (i) {
943
+ this.item = elem.pathSegList.getItem(i)
944
+ this.update()
945
+ })
946
+
947
+ return this
948
+ }
949
+
950
+ /**
951
+ * @param {string} text
952
+ * @returns {void}
953
+ */
954
+ endChanges (text) {
955
+ const cmd = new ChangeElementCommand(this.elem, { d: this.last_d }, text)
956
+ svgCanvas.endChanges({ cmd, elem: this.elem })
957
+ }
958
+
959
+ /**
960
+ * @param {Integer|Integer[]} indexes
961
+ * @returns {void}
962
+ */
963
+ addPtsToSelection (indexes) {
964
+ if (!Array.isArray(indexes)) { indexes = [indexes] }
965
+ indexes.forEach((index) => {
966
+ const seg = this.segs[index]
967
+ if (seg.ptgrip && !this.selected_pts.includes(index) && index >= 0) {
968
+ this.selected_pts.push(index)
969
+ }
970
+ })
971
+ this.selected_pts.sort()
972
+ let i = this.selected_pts.length
973
+ const grips = []
974
+ grips.length = i
975
+ // Loop through points to be selected and highlight each
976
+ while (i--) {
977
+ const pt = this.selected_pts[i]
978
+ const seg = this.segs[pt]
979
+ seg.select(true)
980
+ grips[i] = seg.ptgrip
981
+ }
982
+
983
+ const closedSubpath = Path.subpathIsClosed(this.selected_pts[0])
984
+ svgCanvas.addPtsToSelection({ grips, closedSubpath })
985
+ }
986
+
987
+ // STATIC
988
+ /**
989
+ * @param {Integer} index
990
+ * @returns {boolean}
991
+ */
992
+ static subpathIsClosed (index) {
993
+ let clsd = false
994
+ // Check if subpath is already open
995
+ const path = svgCanvas.getPathObj()
996
+ path.eachSeg(function (i) {
997
+ if (i <= index) { return true }
998
+ if (this.type === 2) {
999
+ // Found M first, so open
1000
+ return false
1001
+ }
1002
+ if (this.type === 1) {
1003
+ // Found Z first, so closed
1004
+ clsd = true
1005
+ return false
1006
+ }
1007
+ return true
1008
+ })
1009
+
1010
+ return clsd
1011
+ }
1012
+ }