@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/event.js ADDED
@@ -0,0 +1,1388 @@
1
+ /**
2
+ * Tools for event.
3
+ * @module event
4
+ * @license MIT
5
+ * @copyright 2011 Jeff Schiller
6
+ */
7
+ import {
8
+ assignAttributes, cleanupElement, getElement, getRotationAngle, snapToGrid, walkTree,
9
+ preventClickDefault, setHref, getBBox
10
+ } from './utilities.js'
11
+ import {
12
+ convertAttrs
13
+ } from '../../src/common/units.js'
14
+ import {
15
+ transformPoint, hasMatrixTransform, getMatrix, snapToAngle
16
+ } from './math.js'
17
+ import * as draw from './draw.js'
18
+ import * as pathModule from './path.js'
19
+ import * as hstry from './history.js'
20
+ import { findPos } from '../../src/common/util.js'
21
+
22
+ const {
23
+ InsertElementCommand
24
+ } = hstry
25
+
26
+ let svgCanvas = null
27
+
28
+ /**
29
+ * @function module:undo.init
30
+ * @param {module:undo.eventContext} eventContext
31
+ * @returns {void}
32
+ */
33
+ export const init = (canvas) => {
34
+ svgCanvas = canvas
35
+ svgCanvas.mouseDownEvent = mouseDownEvent
36
+ svgCanvas.mouseMoveEvent = mouseMoveEvent
37
+ svgCanvas.dblClickEvent = dblClickEvent
38
+ svgCanvas.mouseUpEvent = mouseUpEvent
39
+ svgCanvas.mouseOutEvent = mouseOutEvent
40
+ svgCanvas.DOMMouseScrollEvent = DOMMouseScrollEvent
41
+ }
42
+
43
+ const getBsplinePoint = (t) => {
44
+ const spline = { x: 0, y: 0 }
45
+ const p0 = { x: svgCanvas.getControllPoint2('x'), y: svgCanvas.getControllPoint2('y') }
46
+ const p1 = { x: svgCanvas.getControllPoint1('x'), y: svgCanvas.getControllPoint1('y') }
47
+ const p2 = { x: svgCanvas.getStart('x'), y: svgCanvas.getStart('y') }
48
+ const p3 = { x: svgCanvas.getEnd('x'), y: svgCanvas.getEnd('y') }
49
+ const S = 1.0 / 6.0
50
+ const t2 = t * t
51
+ const t3 = t2 * t
52
+
53
+ const m = [
54
+ [-1, 3, -3, 1],
55
+ [3, -6, 3, 0],
56
+ [-3, 0, 3, 0],
57
+ [1, 4, 1, 0]
58
+ ]
59
+
60
+ spline.x = S * (
61
+ (p0.x * m[0][0] + p1.x * m[0][1] + p2.x * m[0][2] + p3.x * m[0][3]) * t3 +
62
+ (p0.x * m[1][0] + p1.x * m[1][1] + p2.x * m[1][2] + p3.x * m[1][3]) * t2 +
63
+ (p0.x * m[2][0] + p1.x * m[2][1] + p2.x * m[2][2] + p3.x * m[2][3]) * t +
64
+ (p0.x * m[3][0] + p1.x * m[3][1] + p2.x * m[3][2] + p3.x * m[3][3])
65
+ )
66
+ spline.y = S * (
67
+ (p0.y * m[0][0] + p1.y * m[0][1] + p2.y * m[0][2] + p3.y * m[0][3]) * t3 +
68
+ (p0.y * m[1][0] + p1.y * m[1][1] + p2.y * m[1][2] + p3.y * m[1][3]) * t2 +
69
+ (p0.y * m[2][0] + p1.y * m[2][1] + p2.y * m[2][2] + p3.y * m[2][3]) * t +
70
+ (p0.y * m[3][0] + p1.y * m[3][1] + p2.y * m[3][2] + p3.y * m[3][3])
71
+ )
72
+
73
+ return {
74
+ x: spline.x,
75
+ y: spline.y
76
+ }
77
+ }
78
+
79
+ // update the dummy transform in our transform list
80
+ // to be a translate. We need to check if there was a transformation
81
+ // to avoid loosing it
82
+ const updateTransformList = (svgRoot, element, dx, dy) => {
83
+ const xform = svgRoot.createSVGTransform()
84
+ xform.setTranslate(dx, dy)
85
+ const tlist = element.transform?.baseVal
86
+ if (tlist.numberOfItems) {
87
+ const firstItem = tlist.getItem(0)
88
+ if (firstItem.type === 2) { // SVG_TRANSFORM_TRANSLATE = 2
89
+ tlist.replaceItem(xform, 0)
90
+ } else {
91
+ tlist.insertItemBefore(xform, 0)
92
+ }
93
+ } else {
94
+ tlist.appendItem(xform)
95
+ }
96
+ }
97
+
98
+ /**
99
+ *
100
+ * @param {MouseEvent} evt
101
+ * @fires module:svgcanvas.SvgCanvas#event:transition
102
+ * @fires module:svgcanvas.SvgCanvas#event:ext_mouseMove
103
+ * @returns {void}
104
+ */
105
+ const mouseMoveEvent = (evt) => {
106
+ // if the mouse is move without dragging an element, just return.
107
+ if (!svgCanvas.getStarted()) { return }
108
+ if (evt.button === 1 || svgCanvas.spaceKey) { return }
109
+
110
+ svgCanvas.textActions.init()
111
+
112
+ evt.preventDefault()
113
+
114
+ const selectedElements = svgCanvas.getSelectedElements()
115
+ const zoom = svgCanvas.getZoom()
116
+ const svgRoot = svgCanvas.getSvgRoot()
117
+ const selected = selectedElements[0]
118
+
119
+ let i
120
+ let xya
121
+ let cx
122
+ let cy
123
+ let dx
124
+ let dy
125
+ let len
126
+ let angle
127
+ let box
128
+
129
+ const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
130
+ const mouseX = pt.x * zoom
131
+ const mouseY = pt.y * zoom
132
+ const shape = getElement(svgCanvas.getId())
133
+
134
+ let realX = mouseX / zoom
135
+ let x = realX
136
+ let realY = mouseY / zoom
137
+ let y = realY
138
+
139
+ if (svgCanvas.getCurConfig().gridSnapping) {
140
+ x = snapToGrid(x)
141
+ y = snapToGrid(y)
142
+ }
143
+
144
+ let tlist
145
+ switch (svgCanvas.getCurrentMode()) {
146
+ case 'select': {
147
+ // we temporarily use a translate on the element(s) being dragged
148
+ // this transform is removed upon mousing up and the element is
149
+ // relocated to the new location
150
+ if (selected) {
151
+ dx = x - svgCanvas.getStartX()
152
+ dy = y - svgCanvas.getStartY()
153
+ if (svgCanvas.getCurConfig().gridSnapping) {
154
+ dx = snapToGrid(dx)
155
+ dy = snapToGrid(dy)
156
+ }
157
+
158
+ if (dx || dy) {
159
+ selectedElements.forEach((el) => {
160
+ if (el) {
161
+ updateTransformList(svgRoot, el, dx, dy)
162
+ // update our internal bbox that we're tracking while dragging
163
+ svgCanvas.selectorManager.requestSelector(el).resize()
164
+ }
165
+ })
166
+ svgCanvas.call('transition', selectedElements)
167
+ }
168
+ }
169
+ break
170
+ }
171
+ case 'multiselect': {
172
+ realX *= zoom
173
+ realY *= zoom
174
+ assignAttributes(svgCanvas.getRubberBox(), {
175
+ x: Math.min(svgCanvas.getRStartX(), realX),
176
+ y: Math.min(svgCanvas.getRStartY(), realY),
177
+ width: Math.abs(realX - svgCanvas.getRStartX()),
178
+ height: Math.abs(realY - svgCanvas.getRStartY())
179
+ }, 100)
180
+
181
+ // for each selected:
182
+ // - if newList contains selected, do nothing
183
+ // - if newList doesn't contain selected, remove it from selected
184
+ // - for any newList that was not in selectedElements, add it to selected
185
+ const elemsToRemove = selectedElements.slice(); const elemsToAdd = []
186
+ const newList = svgCanvas.getIntersectionList()
187
+
188
+ // For every element in the intersection, add if not present in selectedElements.
189
+ len = newList.length
190
+ for (i = 0; i < len; ++i) {
191
+ const intElem = newList[i]
192
+ // Found an element that was not selected before, so we should add it.
193
+ if (!selectedElements.includes(intElem)) {
194
+ elemsToAdd.push(intElem)
195
+ }
196
+ // Found an element that was already selected, so we shouldn't remove it.
197
+ const foundInd = elemsToRemove.indexOf(intElem)
198
+ if (foundInd !== -1) {
199
+ elemsToRemove.splice(foundInd, 1)
200
+ }
201
+ }
202
+
203
+ if (elemsToRemove.length > 0) {
204
+ svgCanvas.removeFromSelection(elemsToRemove)
205
+ }
206
+
207
+ if (elemsToAdd.length > 0) {
208
+ svgCanvas.addToSelection(elemsToAdd)
209
+ }
210
+
211
+ break
212
+ }
213
+ case 'resize': {
214
+ // we track the resize bounding box and translate/scale the selected element
215
+ // while the mouse is down, when mouse goes up, we use this to recalculate
216
+ // the shape's coordinates
217
+ tlist = selected.transform.baseVal
218
+ const hasMatrix = hasMatrixTransform(tlist)
219
+ box = hasMatrix ? svgCanvas.getInitBbox() : getBBox(selected)
220
+ let left = box.x
221
+ let top = box.y
222
+ let { width, height } = box
223
+ dx = (x - svgCanvas.getStartX())
224
+ dy = (y - svgCanvas.getStartY())
225
+
226
+ if (svgCanvas.getCurConfig().gridSnapping) {
227
+ dx = snapToGrid(dx)
228
+ dy = snapToGrid(dy)
229
+ height = snapToGrid(height)
230
+ width = snapToGrid(width)
231
+ }
232
+
233
+ // if rotated, adjust the dx,dy values
234
+ angle = getRotationAngle(selected)
235
+ if (angle) {
236
+ const r = Math.sqrt(dx * dx + dy * dy)
237
+ const theta = Math.atan2(dy, dx) - angle * Math.PI / 180.0
238
+ dx = r * Math.cos(theta)
239
+ dy = r * Math.sin(theta)
240
+ }
241
+
242
+ // if not stretching in y direction, set dy to 0
243
+ // if not stretching in x direction, set dx to 0
244
+ if (!svgCanvas.getCurrentResizeMode().includes('n') && !svgCanvas.getCurrentResizeMode().includes('s')) {
245
+ dy = 0
246
+ }
247
+ if (!svgCanvas.getCurrentResizeMode().includes('e') && !svgCanvas.getCurrentResizeMode().includes('w')) {
248
+ dx = 0
249
+ }
250
+
251
+ let // ts = null,
252
+ tx = 0; let ty = 0
253
+ let sy = height ? (height + dy) / height : 1
254
+ let sx = width ? (width + dx) / width : 1
255
+ // if we are dragging on the north side, then adjust the scale factor and ty
256
+ if (svgCanvas.getCurrentResizeMode().includes('n')) {
257
+ sy = height ? (height - dy) / height : 1
258
+ ty = height
259
+ }
260
+
261
+ // if we dragging on the east side, then adjust the scale factor and tx
262
+ if (svgCanvas.getCurrentResizeMode().includes('w')) {
263
+ sx = width ? (width - dx) / width : 1
264
+ tx = width
265
+ }
266
+
267
+ // update the transform list with translate,scale,translate
268
+ const translateOrigin = svgRoot.createSVGTransform()
269
+ const scale = svgRoot.createSVGTransform()
270
+ const translateBack = svgRoot.createSVGTransform()
271
+
272
+ if (svgCanvas.getCurConfig().gridSnapping) {
273
+ left = snapToGrid(left)
274
+ tx = snapToGrid(tx)
275
+ top = snapToGrid(top)
276
+ ty = snapToGrid(ty)
277
+ }
278
+
279
+ translateOrigin.setTranslate(-(left + tx), -(top + ty))
280
+ if (evt.shiftKey) {
281
+ if (sx === 1) {
282
+ sx = sy
283
+ } else { sy = sx }
284
+ }
285
+ scale.setScale(sx, sy)
286
+
287
+ translateBack.setTranslate(left + tx, top + ty)
288
+ if (hasMatrix) {
289
+ const diff = angle ? 1 : 0
290
+ tlist.replaceItem(translateOrigin, 2 + diff)
291
+ tlist.replaceItem(scale, 1 + diff)
292
+ tlist.replaceItem(translateBack, Number(diff))
293
+ } else {
294
+ const N = tlist.numberOfItems
295
+ tlist.replaceItem(translateBack, N - 3)
296
+ tlist.replaceItem(scale, N - 2)
297
+ tlist.replaceItem(translateOrigin, N - 1)
298
+ }
299
+
300
+ svgCanvas.selectorManager.requestSelector(selected).resize()
301
+ svgCanvas.call('transition', selectedElements)
302
+
303
+ break
304
+ }
305
+ case 'zoom': {
306
+ realX *= zoom
307
+ realY *= zoom
308
+ assignAttributes(svgCanvas.getRubberBox(), {
309
+ x: Math.min(svgCanvas.getRStartX() * zoom, realX),
310
+ y: Math.min(svgCanvas.getRStartY() * zoom, realY),
311
+ width: Math.abs(realX - svgCanvas.getRStartX() * zoom),
312
+ height: Math.abs(realY - svgCanvas.getRStartY() * zoom)
313
+ }, 100)
314
+ break
315
+ }
316
+ case 'text': {
317
+ assignAttributes(shape, {
318
+ x,
319
+ y
320
+ }, 1000)
321
+ break
322
+ }
323
+ case 'line': {
324
+ if (svgCanvas.getCurConfig().gridSnapping) {
325
+ x = snapToGrid(x)
326
+ y = snapToGrid(y)
327
+ }
328
+
329
+ let x2 = x
330
+ let y2 = y
331
+
332
+ if (evt.shiftKey) {
333
+ xya = snapToAngle(svgCanvas.getStartX(), svgCanvas.getStartY(), x2, y2)
334
+ x2 = xya.x
335
+ y2 = xya.y
336
+ }
337
+
338
+ shape.setAttribute('x2', x2)
339
+ shape.setAttribute('y2', y2)
340
+ break
341
+ }
342
+ case 'foreignObject': // fall through
343
+ case 'square':
344
+ case 'rect':
345
+ case 'image': {
346
+ const square = (svgCanvas.getCurrentMode() === 'square') || evt.shiftKey
347
+ let
348
+ w = Math.abs(x - svgCanvas.getStartX())
349
+ let h = Math.abs(y - svgCanvas.getStartY())
350
+ let newX; let newY
351
+ if (square) {
352
+ w = h = Math.max(w, h)
353
+ newX = svgCanvas.getStartX() < x ? svgCanvas.getStartX() : svgCanvas.getStartX() - w
354
+ newY = svgCanvas.getStartY() < y ? svgCanvas.getStartY() : svgCanvas.getStartY() - h
355
+ } else {
356
+ newX = Math.min(svgCanvas.getStartX(), x)
357
+ newY = Math.min(svgCanvas.getStartY(), y)
358
+ }
359
+
360
+ if (svgCanvas.getCurConfig().gridSnapping) {
361
+ w = snapToGrid(w)
362
+ h = snapToGrid(h)
363
+ newX = snapToGrid(newX)
364
+ newY = snapToGrid(newY)
365
+ }
366
+
367
+ assignAttributes(shape, {
368
+ width: w,
369
+ height: h,
370
+ x: newX,
371
+ y: newY
372
+ }, 1000)
373
+
374
+ break
375
+ }
376
+ case 'circle': {
377
+ cx = Number(shape.getAttribute('cx'))
378
+ cy = Number(shape.getAttribute('cy'))
379
+ let rad = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))
380
+ if (svgCanvas.getCurConfig().gridSnapping) {
381
+ rad = snapToGrid(rad)
382
+ }
383
+ shape.setAttribute('r', rad)
384
+ break
385
+ }
386
+ case 'ellipse': {
387
+ cx = Number(shape.getAttribute('cx'))
388
+ cy = Number(shape.getAttribute('cy'))
389
+ if (svgCanvas.getCurConfig().gridSnapping) {
390
+ x = snapToGrid(x)
391
+ cx = snapToGrid(cx)
392
+ y = snapToGrid(y)
393
+ cy = snapToGrid(cy)
394
+ }
395
+ shape.setAttribute('rx', Math.abs(x - cx))
396
+ const ry = Math.abs(evt.shiftKey ? (x - cx) : (y - cy))
397
+ shape.setAttribute('ry', ry)
398
+ break
399
+ }
400
+ case 'fhellipse':
401
+ case 'fhrect': {
402
+ svgCanvas.setFreehand('minx', Math.min(realX, svgCanvas.getFreehand('minx')))
403
+ svgCanvas.setFreehand('maxx', Math.max(realX, svgCanvas.getFreehand('maxx')))
404
+ svgCanvas.setFreehand('miny', Math.min(realY, svgCanvas.getFreehand('miny')))
405
+ svgCanvas.setFreehand('maxy', Math.max(realY, svgCanvas.getFreehand('maxy')))
406
+ }
407
+ // Fallthrough
408
+ case 'fhpath': {
409
+ // dAttr += + realX + ',' + realY + ' ';
410
+ // shape.setAttribute('points', dAttr);
411
+ svgCanvas.setEnd('x', realX)
412
+ svgCanvas.setEnd('y', realY)
413
+ if (svgCanvas.getControllPoint2('x') && svgCanvas.getControllPoint2('y')) {
414
+ for (i = 0; i < svgCanvas.getStepCount() - 1; i++) {
415
+ svgCanvas.setParameter(i / svgCanvas.getStepCount())
416
+ svgCanvas.setNextParameter((i + 1) / svgCanvas.getStepCount())
417
+ svgCanvas.setbSpline(getBsplinePoint(svgCanvas.getNextParameter()))
418
+ svgCanvas.setNextPos({ x: svgCanvas.getbSpline('x'), y: svgCanvas.getbSpline('y') })
419
+ svgCanvas.setbSpline(getBsplinePoint(svgCanvas.getParameter()))
420
+ svgCanvas.setSumDistance(
421
+ svgCanvas.getSumDistance() + Math.sqrt((svgCanvas.getNextPos('x') -
422
+ svgCanvas.getbSpline('x')) * (svgCanvas.getNextPos('x') -
423
+ svgCanvas.getbSpline('x')) + (svgCanvas.getNextPos('y') -
424
+ svgCanvas.getbSpline('y')) * (svgCanvas.getNextPos('y') - svgCanvas.getbSpline('y')))
425
+ )
426
+ if (svgCanvas.getSumDistance() > svgCanvas.getThreSholdDist()) {
427
+ svgCanvas.setSumDistance(svgCanvas.getSumDistance() - svgCanvas.getThreSholdDist())
428
+
429
+ // Faster than completely re-writing the points attribute.
430
+ const point = svgCanvas.getSvgContent().createSVGPoint()
431
+ point.x = svgCanvas.getbSpline('x')
432
+ point.y = svgCanvas.getbSpline('y')
433
+ shape.points.appendItem(point)
434
+ }
435
+ }
436
+ }
437
+ svgCanvas.setControllPoint2('x', svgCanvas.getControllPoint1('x'))
438
+ svgCanvas.setControllPoint2('y', svgCanvas.getControllPoint1('y'))
439
+ svgCanvas.setControllPoint1('x', svgCanvas.getStart('x'))
440
+ svgCanvas.setControllPoint1('y', svgCanvas.getStart('y'))
441
+ svgCanvas.setStart({ x: svgCanvas.getEnd('x'), y: svgCanvas.getEnd('y') })
442
+ break
443
+ // update path stretch line coordinates
444
+ }
445
+ case 'path': // fall through
446
+ case 'pathedit': {
447
+ x *= zoom
448
+ y *= zoom
449
+
450
+ if (svgCanvas.getCurConfig().gridSnapping) {
451
+ x = snapToGrid(x)
452
+ y = snapToGrid(y)
453
+ svgCanvas.setStartX(snapToGrid(svgCanvas.getStartX()))
454
+ svgCanvas.setStartY(snapToGrid(svgCanvas.getStartY()))
455
+ }
456
+ if (evt.shiftKey) {
457
+ const { path } = pathModule
458
+ let x1; let y1
459
+ if (path) {
460
+ x1 = path.dragging ? path.dragging[0] : svgCanvas.getStartX()
461
+ y1 = path.dragging ? path.dragging[1] : svgCanvas.getStartY()
462
+ } else {
463
+ x1 = svgCanvas.getStartX()
464
+ y1 = svgCanvas.getStartY()
465
+ }
466
+ xya = snapToAngle(x1, y1, x, y);
467
+ ({ x, y } = xya)
468
+ }
469
+
470
+ if (svgCanvas.getRubberBox()?.getAttribute('display') !== 'none') {
471
+ realX *= zoom
472
+ realY *= zoom
473
+ assignAttributes(svgCanvas.getRubberBox(), {
474
+ x: Math.min(svgCanvas.getRStartX() * zoom, realX),
475
+ y: Math.min(svgCanvas.getRStartY() * zoom, realY),
476
+ width: Math.abs(realX - svgCanvas.getRStartX() * zoom),
477
+ height: Math.abs(realY - svgCanvas.getRStartY() * zoom)
478
+ }, 100)
479
+ }
480
+ svgCanvas.pathActions.mouseMove(x, y)
481
+
482
+ break
483
+ }
484
+ case 'textedit': {
485
+ x *= zoom
486
+ y *= zoom
487
+ svgCanvas.textActions.mouseMove(mouseX, mouseY)
488
+
489
+ break
490
+ }
491
+ case 'rotate': {
492
+ box = getBBox(selected)
493
+ cx = box.x + box.width / 2
494
+ cy = box.y + box.height / 2
495
+ const m = getMatrix(selected)
496
+ const center = transformPoint(cx, cy, m)
497
+ cx = center.x
498
+ cy = center.y
499
+ angle = ((Math.atan2(cy - y, cx - x) * (180 / Math.PI)) - 90) % 360
500
+ if (svgCanvas.getCurConfig().gridSnapping) {
501
+ angle = snapToGrid(angle)
502
+ }
503
+ if (evt.shiftKey) { // restrict rotations to nice angles (WRS)
504
+ const snap = 45
505
+ angle = Math.round(angle / snap) * snap
506
+ }
507
+
508
+ svgCanvas.setRotationAngle(angle < -180 ? (360 + angle) : angle, true)
509
+ svgCanvas.call('transition', selectedElements)
510
+ break
511
+ }
512
+ default:
513
+ // A mode can be defined by an extenstion
514
+ break
515
+ }
516
+
517
+ /**
518
+ * The mouse has moved on the canvas area.
519
+ * @event module:svgcanvas.SvgCanvas#event:ext_mouseMove
520
+ * @type {PlainObject}
521
+ * @property {MouseEvent} event The event object
522
+ * @property {Float} mouse_x x coordinate on canvas
523
+ * @property {Float} mouse_y y coordinate on canvas
524
+ * @property {Element} selected Refers to the first selected element
525
+ */
526
+ svgCanvas.runExtensions('mouseMove', /** @type {module:svgcanvas.SvgCanvas#event:ext_mouseMove} */ {
527
+ event: evt,
528
+ mouse_x: mouseX,
529
+ mouse_y: mouseY,
530
+ selected
531
+ })
532
+ } // mouseMove()
533
+
534
+ /**
535
+ *
536
+ * @returns {void}
537
+ */
538
+ const mouseOutEvent = () => {
539
+ const { $id } = svgCanvas
540
+ if (svgCanvas.getCurrentMode() !== 'select' && svgCanvas.getStarted()) {
541
+ const event = new Event('mouseup')
542
+ $id('svgcanvas').dispatchEvent(event)
543
+ }
544
+ }
545
+
546
+ // - in create mode, the element's opacity is set properly, we create an InsertElementCommand
547
+ // and store it on the Undo stack
548
+ // - in move/resize mode, the element's attributes which were affected by the move/resize are
549
+ // identified, a ChangeElementCommand is created and stored on the stack for those attrs
550
+ // this is done in when we recalculate the selected dimensions()
551
+ /**
552
+ *
553
+ * @param {MouseEvent} evt
554
+ * @fires module:svgcanvas.SvgCanvas#event:zoomed
555
+ * @fires module:svgcanvas.SvgCanvas#event:changed
556
+ * @fires module:svgcanvas.SvgCanvas#event:ext_mouseUp
557
+ * @returns {void}
558
+ */
559
+ const mouseUpEvent = (evt) => {
560
+ if (evt.button === 2) { return }
561
+ if (!svgCanvas.getStarted()) { return }
562
+
563
+ svgCanvas.textActions.init()
564
+
565
+ const selectedElements = svgCanvas.getSelectedElements()
566
+ const zoom = svgCanvas.getZoom()
567
+
568
+ const tempJustSelected = svgCanvas.getJustSelected()
569
+ svgCanvas.setJustSelected(null)
570
+
571
+ const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
572
+ const mouseX = pt.x * zoom
573
+ const mouseY = pt.y * zoom
574
+ const x = mouseX / zoom
575
+ const y = mouseY / zoom
576
+
577
+ let element = getElement(svgCanvas.getId())
578
+ let keep = false
579
+
580
+ const realX = x
581
+ const realY = y
582
+
583
+ // TODO: Make true when in multi-unit mode
584
+ const useUnit = false // (svgCanvas.getCurConfig().baseUnit !== 'px');
585
+ svgCanvas.setStarted(false)
586
+ let t
587
+ switch (svgCanvas.getCurrentMode()) {
588
+ // intentionally fall-through to select here
589
+ case 'resize':
590
+ case 'multiselect':
591
+ if (svgCanvas.getRubberBox()) {
592
+ svgCanvas.getRubberBox().setAttribute('display', 'none')
593
+ svgCanvas.setCurBBoxes([])
594
+ }
595
+ svgCanvas.setCurrentMode('select')
596
+ // Fallthrough
597
+ case 'select':
598
+ if (selectedElements[0]) {
599
+ // if we only have one selected element
600
+ if (!selectedElements[1]) {
601
+ // set our current stroke/fill properties to the element's
602
+ const selected = selectedElements[0]
603
+ switch (selected.tagName) {
604
+ case 'g':
605
+ case 'use':
606
+ case 'image':
607
+ case 'foreignObject':
608
+ break
609
+ case 'text':
610
+ svgCanvas.setCurText('font_size', selected.getAttribute('font-size'))
611
+ svgCanvas.setCurText('font_family', selected.getAttribute('font-family'))
612
+ // fallthrough
613
+ default:
614
+ svgCanvas.setCurProperties('fill', selected.getAttribute('fill'))
615
+ svgCanvas.setCurProperties('fill_opacity', selected.getAttribute('fill-opacity'))
616
+ svgCanvas.setCurProperties('stroke', selected.getAttribute('stroke'))
617
+ svgCanvas.setCurProperties('stroke_opacity', selected.getAttribute('stroke-opacity'))
618
+ svgCanvas.setCurProperties('stroke_width', selected.getAttribute('stroke-width'))
619
+ svgCanvas.setCurProperties('stroke_dasharray', selected.getAttribute('stroke-dasharray'))
620
+ svgCanvas.setCurProperties('stroke_linejoin', selected.getAttribute('stroke-linejoin'))
621
+ svgCanvas.setCurProperties('stroke_linecap', selected.getAttribute('stroke-linecap'))
622
+ }
623
+ svgCanvas.selectorManager.requestSelector(selected).showGrips(true)
624
+ }
625
+ // always recalculate dimensions to strip off stray identity transforms
626
+ svgCanvas.recalculateAllSelectedDimensions()
627
+ // if it was being dragged/resized
628
+ if (realX !== svgCanvas.getRStartX() || realY !== svgCanvas.getRStartY()) {
629
+ const len = selectedElements.length
630
+ for (let i = 0; i < len; ++i) {
631
+ if (!selectedElements[i]) { break }
632
+ svgCanvas.selectorManager.requestSelector(selectedElements[i]).resize()
633
+ }
634
+ // no change in position/size, so maybe we should move to pathedit
635
+ } else {
636
+ t = evt.target
637
+ if (selectedElements[0].nodeName === 'path' && !selectedElements[1]) {
638
+ svgCanvas.pathActions.select(selectedElements[0])
639
+ // if it was a path
640
+ // else, if it was selected and this is a shift-click, remove it from selection
641
+ } else if (evt.shiftKey && tempJustSelected !== t) {
642
+ svgCanvas.removeFromSelection([t])
643
+ }
644
+ } // no change in mouse position
645
+
646
+ // Remove non-scaling stroke
647
+ const elem = selectedElements[0]
648
+ if (elem) {
649
+ elem.removeAttribute('style')
650
+ walkTree(elem, (el) => {
651
+ el.removeAttribute('style')
652
+ })
653
+ }
654
+ }
655
+ return
656
+ case 'zoom': {
657
+ svgCanvas.getRubberBox()?.setAttribute('display', 'none')
658
+ const factor = evt.shiftKey ? 0.5 : 2
659
+ svgCanvas.call('zoomed', {
660
+ x: Math.min(svgCanvas.getRStartX(), realX),
661
+ y: Math.min(svgCanvas.getRStartY(), realY),
662
+ width: Math.abs(realX - svgCanvas.getRStartX()),
663
+ height: Math.abs(realY - svgCanvas.getRStartY()),
664
+ factor
665
+ })
666
+ return
667
+ } case 'fhpath': {
668
+ // Check that the path contains at least 2 points; a degenerate one-point path
669
+ // causes problems.
670
+ // Webkit ignores how we set the points attribute with commas and uses space
671
+ // to separate all coordinates, see https://bugs.webkit.org/show_bug.cgi?id=29870
672
+ svgCanvas.setSumDistance(0)
673
+ svgCanvas.setControllPoint2('x', 0)
674
+ svgCanvas.setControllPoint2('y', 0)
675
+ svgCanvas.setControllPoint1('x', 0)
676
+ svgCanvas.setControllPoint1('y', 0)
677
+ svgCanvas.setStart({ x: 0, y: 0 })
678
+ svgCanvas.setEnd('x', 0)
679
+ svgCanvas.setEnd('y', 0)
680
+ const coords = element.getAttribute('points')
681
+ const commaIndex = coords.indexOf(',')
682
+ keep = commaIndex >= 0 ? coords.includes(',', commaIndex + 1) : coords.includes(' ', coords.indexOf(' ') + 1)
683
+ if (keep) {
684
+ element = svgCanvas.pathActions.smoothPolylineIntoPath(element)
685
+ }
686
+ break
687
+ } case 'line': {
688
+ const x1 = element.getAttribute('x1')
689
+ const y1 = element.getAttribute('y1')
690
+ const x2 = element.getAttribute('x2')
691
+ const y2 = element.getAttribute('y2')
692
+ keep = (x1 !== x2 || y1 !== y2)
693
+ }
694
+ break
695
+ case 'foreignObject':
696
+ case 'square':
697
+ case 'rect':
698
+ case 'image': {
699
+ const width = element.getAttribute('width')
700
+ const height = element.getAttribute('height')
701
+ // Image should be kept regardless of size (use inherit dimensions later)
702
+ keep = (width || height) || svgCanvas.getCurrentMode() === 'image'
703
+ }
704
+ break
705
+ case 'circle':
706
+ keep = (element.getAttribute('r') !== '0')
707
+ break
708
+ case 'ellipse': {
709
+ const rx = Number(element.getAttribute('rx'))
710
+ const ry = Number(element.getAttribute('ry'))
711
+ keep = (rx || ry)
712
+ }
713
+ break
714
+ case 'fhellipse':
715
+ if ((svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) > 0 &&
716
+ (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) > 0) {
717
+ element = svgCanvas.addSVGElementsFromJson({
718
+ element: 'ellipse',
719
+ curStyles: true,
720
+ attr: {
721
+ cx: (svgCanvas.getFreehand('minx') + svgCanvas.getFreehand('maxx')) / 2,
722
+ cy: (svgCanvas.getFreehand('miny') + svgCanvas.getFreehand('maxy')) / 2,
723
+ rx: (svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) / 2,
724
+ ry: (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) / 2,
725
+ id: svgCanvas.getId()
726
+ }
727
+ })
728
+ svgCanvas.call('changed', [element])
729
+ keep = true
730
+ }
731
+ break
732
+ case 'fhrect':
733
+ if ((svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) > 0 &&
734
+ (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) > 0) {
735
+ element = svgCanvas.addSVGElementsFromJson({
736
+ element: 'rect',
737
+ curStyles: true,
738
+ attr: {
739
+ x: svgCanvas.getFreehand('minx'),
740
+ y: svgCanvas.getFreehand('miny'),
741
+ width: (svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')),
742
+ height: (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')),
743
+ id: svgCanvas.getId()
744
+ }
745
+ })
746
+ svgCanvas.call('changed', [element])
747
+ keep = true
748
+ }
749
+ break
750
+ case 'text':
751
+ keep = true
752
+ svgCanvas.selectOnly([element])
753
+ svgCanvas.textActions.start(element)
754
+ break
755
+ case 'path': {
756
+ // set element to null here so that it is not removed nor finalized
757
+ element = null
758
+ // continue to be set to true so that mouseMove happens
759
+ svgCanvas.setStarted(true)
760
+
761
+ const res = svgCanvas.pathActions.mouseUp(evt, element, mouseX, mouseY);
762
+ ({ element } = res);
763
+ ({ keep } = res)
764
+ break
765
+ } case 'pathedit':
766
+ keep = true
767
+ element = null
768
+ svgCanvas.pathActions.mouseUp(evt)
769
+ break
770
+ case 'textedit':
771
+ keep = false
772
+ element = null
773
+ svgCanvas.textActions.mouseUp(evt, mouseX, mouseY)
774
+ break
775
+ case 'rotate': {
776
+ keep = true
777
+ element = null
778
+ svgCanvas.setCurrentMode('select')
779
+ const batchCmd = svgCanvas.undoMgr.finishUndoableChange()
780
+ if (!batchCmd.isEmpty()) {
781
+ svgCanvas.addCommandToHistory(batchCmd)
782
+ }
783
+ // perform recalculation to weed out any stray identity transforms that might get stuck
784
+ svgCanvas.recalculateAllSelectedDimensions()
785
+ svgCanvas.call('changed', selectedElements)
786
+ break
787
+ } default:
788
+ // This could occur in an extension
789
+ break
790
+ }
791
+
792
+ /**
793
+ * The main (left) mouse button is released (anywhere).
794
+ * @event module:svgcanvas.SvgCanvas#event:ext_mouseUp
795
+ * @type {PlainObject}
796
+ * @property {MouseEvent} event The event object
797
+ * @property {Float} mouse_x x coordinate on canvas
798
+ * @property {Float} mouse_y y coordinate on canvas
799
+ */
800
+ const extResult = svgCanvas.runExtensions('mouseUp', {
801
+ event: evt,
802
+ mouse_x: mouseX,
803
+ mouse_y: mouseY
804
+ }, true)
805
+
806
+ extResult.forEach((r) => {
807
+ if (r) {
808
+ keep = r.keep || keep;
809
+ ({ element } = r)
810
+ svgCanvas.setStarted(r.started || svgCanvas.getStarted())
811
+ }
812
+ })
813
+
814
+ if (!keep && element) {
815
+ svgCanvas.getCurrentDrawing().releaseId(svgCanvas.getId())
816
+ element.remove()
817
+ element = null
818
+
819
+ t = evt.target
820
+
821
+ // if this element is in a group, go up until we reach the top-level group
822
+ // just below the layer groups
823
+ // TODO: once we implement links, we also would have to check for <a> elements
824
+ while (t?.parentNode?.parentNode?.tagName === 'g') {
825
+ t = t.parentNode
826
+ }
827
+ // if we are not in the middle of creating a path, and we've clicked on some shape,
828
+ // then go to Select mode.
829
+ // WebKit returns <div> when the canvas is clicked, Firefox/Opera return <svg>
830
+ if ((svgCanvas.getCurrentMode() !== 'path' || !svgCanvas.getDrawnPath()) &&
831
+ t &&
832
+ t.parentNode?.id !== 'selectorParentGroup' &&
833
+ t.id !== 'svgcanvas' && t.id !== 'svgroot'
834
+ ) {
835
+ // switch into "select" mode if we've clicked on an element
836
+ svgCanvas.setMode('select')
837
+ svgCanvas.selectOnly([t], true)
838
+ }
839
+ } else if (element) {
840
+ /**
841
+ * @name module:svgcanvas.SvgCanvas#addedNew
842
+ * @type {boolean}
843
+ */
844
+ svgCanvas.addedNew = true
845
+
846
+ if (useUnit) { convertAttrs(element) }
847
+
848
+ let aniDur = 0.2
849
+ let cAni
850
+ const curShape = svgCanvas.getStyle()
851
+ const opacAni = svgCanvas.getOpacAni()
852
+ if (opacAni.beginElement && Number.parseFloat(element.getAttribute('opacity')) !== curShape.opacity) {
853
+ cAni = opacAni.cloneNode(true)
854
+ cAni.setAttribute('to', curShape.opacity)
855
+ cAni.setAttribute('dur', aniDur)
856
+ element.appendChild(cAni)
857
+ try {
858
+ // Fails in FF4 on foreignObject
859
+ cAni.beginElement()
860
+ } catch (e) { /* empty fn */ }
861
+ } else {
862
+ aniDur = 0
863
+ }
864
+
865
+ // Ideally this would be done on the endEvent of the animation,
866
+ // but that doesn't seem to be supported in Webkit
867
+ setTimeout(() => {
868
+ if (cAni) { cAni.remove() }
869
+ element.setAttribute('opacity', curShape.opacity)
870
+ element.setAttribute('style', 'pointer-events:inherit')
871
+ cleanupElement(element)
872
+ if (svgCanvas.getCurrentMode() === 'path') {
873
+ svgCanvas.pathActions.toEditMode(element)
874
+ } else if (svgCanvas.getCurConfig().selectNew) {
875
+ const modes = ['circle', 'ellipse', 'square', 'rect', 'fhpath', 'line', 'fhellipse', 'fhrect', 'star', 'polygon']
876
+ if (modes.indexOf(svgCanvas.getCurrentMode()) !== -1) {
877
+ svgCanvas.setMode('select')
878
+ }
879
+ svgCanvas.selectOnly([element], true)
880
+ }
881
+ // we create the insert command that is stored on the stack
882
+ // undo means to call cmd.unapply(), redo means to call cmd.apply()
883
+ svgCanvas.addCommandToHistory(new InsertElementCommand(element))
884
+ svgCanvas.call('changed', [element])
885
+ }, aniDur * 1000)
886
+ }
887
+ svgCanvas.setStartTransform(null)
888
+ }
889
+
890
+ const dblClickEvent = (evt) => {
891
+ const selectedElements = svgCanvas.getSelectedElements()
892
+ const evtTarget = evt.target
893
+ const parent = evtTarget.parentNode
894
+
895
+ let mouseTarget = svgCanvas.getMouseTarget(evt)
896
+ const { tagName } = mouseTarget
897
+
898
+ if (tagName === 'text' && svgCanvas.getCurrentMode() !== 'textedit') {
899
+ const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
900
+ svgCanvas.textActions.select(mouseTarget, pt.x, pt.y)
901
+ }
902
+
903
+ // Do nothing if already in current group
904
+ if (parent === svgCanvas.getCurrentGroup()) { return }
905
+
906
+ if ((tagName === 'g' || tagName === 'a') && getRotationAngle(mouseTarget)) {
907
+ // TODO: Allow method of in-group editing without having to do
908
+ // this (similar to editing rotated paths)
909
+
910
+ // Ungroup and regroup
911
+ svgCanvas.pushGroupProperties(mouseTarget)
912
+ mouseTarget = selectedElements[0]
913
+ svgCanvas.clearSelection(true)
914
+ }
915
+ // Reset context
916
+ if (svgCanvas.getCurrentGroup()) {
917
+ draw.leaveContext()
918
+ }
919
+
920
+ if ((parent.tagName !== 'g' && parent.tagName !== 'a') ||
921
+ parent === svgCanvas.getCurrentDrawing().getCurrentLayer() ||
922
+ mouseTarget === svgCanvas.selectorManager.selectorParentGroup
923
+ ) {
924
+ // Escape from in-group edit
925
+ return
926
+ }
927
+ draw.setContext(mouseTarget)
928
+ }
929
+
930
+ /**
931
+ * Follows these conditions:
932
+ * - When we are in a create mode, the element is added to the canvas but the
933
+ * action is not recorded until mousing up.
934
+ * - When we are in select mode, select the element, remember the position
935
+ * and do nothing else.
936
+ * @param {MouseEvent} evt
937
+ * @fires module:svgcanvas.SvgCanvas#event:ext_mouseDown
938
+ * @returns {void}
939
+ */
940
+ const mouseDownEvent = (evt) => {
941
+ const dataStorage = svgCanvas.getDataStorage()
942
+ const selectedElements = svgCanvas.getSelectedElements()
943
+ const zoom = svgCanvas.getZoom()
944
+ const curShape = svgCanvas.getStyle()
945
+ const svgRoot = svgCanvas.getSvgRoot()
946
+ const { $id } = svgCanvas
947
+
948
+ if (svgCanvas.spaceKey || evt.button === 1) { return }
949
+
950
+ const rightClick = (evt.button === 2)
951
+
952
+ if (evt.altKey) { // duplicate when dragging
953
+ svgCanvas.cloneSelectedElements(0, 0)
954
+ }
955
+
956
+ svgCanvas.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse())
957
+
958
+ const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
959
+ const mouseX = pt.x * zoom
960
+ const mouseY = pt.y * zoom
961
+
962
+ evt.preventDefault()
963
+
964
+ if (rightClick) {
965
+ if (svgCanvas.getCurrentMode() === 'path') {
966
+ return
967
+ }
968
+ svgCanvas.setCurrentMode('select')
969
+ svgCanvas.setLastClickPoint(pt)
970
+ }
971
+
972
+ let x = mouseX / zoom
973
+ let y = mouseY / zoom
974
+ let mouseTarget = svgCanvas.getMouseTarget(evt)
975
+
976
+ if (mouseTarget.tagName === 'a' && mouseTarget.childNodes.length === 1) {
977
+ mouseTarget = mouseTarget.firstChild
978
+ }
979
+
980
+ // realX/y ignores grid-snap value
981
+ const realX = x
982
+ svgCanvas.setStartX(x)
983
+ svgCanvas.setRStartX(x)
984
+ const realY = y
985
+ svgCanvas.setStartY(y)
986
+ svgCanvas.setRStartY(y)
987
+
988
+ if (svgCanvas.getCurConfig().gridSnapping) {
989
+ x = snapToGrid(x)
990
+ y = snapToGrid(y)
991
+ svgCanvas.setStartX(snapToGrid(svgCanvas.getStartX()))
992
+ svgCanvas.setStartY(snapToGrid(svgCanvas.getStartY()))
993
+ }
994
+
995
+ // if it is a selector grip, then it must be a single element selected,
996
+ // set the mouseTarget to that and update the mode to rotate/resize
997
+
998
+ if (mouseTarget === svgCanvas.selectorManager.selectorParentGroup && selectedElements[0]) {
999
+ const grip = evt.target
1000
+ const griptype = dataStorage.get(grip, 'type')
1001
+ // rotating
1002
+ if (griptype === 'rotate') {
1003
+ svgCanvas.setCurrentMode('rotate')
1004
+ // svgCanvas.setCurrentRotateMode(dataStorage.get(grip, 'dir'));
1005
+ // resizing
1006
+ } else if (griptype === 'resize') {
1007
+ svgCanvas.setCurrentMode('resize')
1008
+ svgCanvas.setCurrentResizeMode(dataStorage.get(grip, 'dir'))
1009
+ }
1010
+ mouseTarget = selectedElements[0]
1011
+ }
1012
+
1013
+ svgCanvas.setStartTransform(mouseTarget.getAttribute('transform'))
1014
+
1015
+ const tlist = mouseTarget.transform.baseVal
1016
+ // consolidate transforms using standard SVG but keep the transformation used for the move/scale
1017
+ if (tlist.numberOfItems > 1) {
1018
+ const firstTransform = tlist.getItem(0)
1019
+ tlist.removeItem(0)
1020
+ tlist.consolidate()
1021
+ tlist.insertItemBefore(firstTransform, 0)
1022
+ }
1023
+ switch (svgCanvas.getCurrentMode()) {
1024
+ case 'select':
1025
+ svgCanvas.setStarted(true)
1026
+ svgCanvas.setCurrentResizeMode('none')
1027
+ if (rightClick) { svgCanvas.setStarted(false) }
1028
+
1029
+ if (mouseTarget !== svgRoot) {
1030
+ // if this element is not yet selected, clear selection and select it
1031
+ if (!selectedElements.includes(mouseTarget)) {
1032
+ // only clear selection if shift is not pressed (otherwise, add
1033
+ // element to selection)
1034
+ if (!evt.shiftKey) {
1035
+ // No need to do the call here as it will be done on addToSelection
1036
+ svgCanvas.clearSelection(true)
1037
+ }
1038
+ svgCanvas.addToSelection([mouseTarget])
1039
+ svgCanvas.setJustSelected(mouseTarget)
1040
+ svgCanvas.pathActions.clear()
1041
+ }
1042
+ // else if it's a path, go into pathedit mode in mouseup
1043
+
1044
+ if (!rightClick) {
1045
+ // insert a dummy transform so if the element(s) are moved it will have
1046
+ // a transform to use for its translate
1047
+ for (const selectedElement of selectedElements) {
1048
+ if (!selectedElement) { continue }
1049
+ const slist = selectedElement.transform?.baseVal
1050
+ if (slist.numberOfItems) {
1051
+ slist.insertItemBefore(svgRoot.createSVGTransform(), 0)
1052
+ } else {
1053
+ slist.appendItem(svgRoot.createSVGTransform())
1054
+ }
1055
+ }
1056
+ }
1057
+ } else if (!rightClick) {
1058
+ svgCanvas.clearSelection()
1059
+ svgCanvas.setCurrentMode('multiselect')
1060
+ if (!svgCanvas.getRubberBox()) {
1061
+ svgCanvas.setRubberBox(svgCanvas.selectorManager.getRubberBandBox())
1062
+ }
1063
+ svgCanvas.setRStartX(svgCanvas.getRStartX() * zoom)
1064
+ svgCanvas.setRStartY(svgCanvas.getRStartY() * zoom)
1065
+
1066
+ assignAttributes(svgCanvas.getRubberBox(), {
1067
+ x: svgCanvas.getRStartX(),
1068
+ y: svgCanvas.getRStartY(),
1069
+ width: 0,
1070
+ height: 0,
1071
+ display: 'inline'
1072
+ }, 100)
1073
+ }
1074
+ break
1075
+ case 'zoom':
1076
+ svgCanvas.setStarted(true)
1077
+ if (!svgCanvas.getRubberBox()) {
1078
+ svgCanvas.setRubberBox(svgCanvas.selectorManager.getRubberBandBox())
1079
+ }
1080
+ assignAttributes(svgCanvas.getRubberBox(), {
1081
+ x: realX * zoom,
1082
+ y: realX * zoom,
1083
+ width: 0,
1084
+ height: 0,
1085
+ display: 'inline'
1086
+ }, 100)
1087
+ break
1088
+ case 'resize': {
1089
+ svgCanvas.setStarted(true)
1090
+ svgCanvas.setStartX(x)
1091
+ svgCanvas.setStartY(y)
1092
+
1093
+ // Getting the BBox from the selection box, since we know we
1094
+ // want to orient around it
1095
+ svgCanvas.setInitBbox(getBBox($id('selectedBox0')))
1096
+ const bb = {}
1097
+ for (const [key, val] of Object.entries(svgCanvas.getInitBbox())) {
1098
+ bb[key] = val / zoom
1099
+ }
1100
+ svgCanvas.setInitBbox(bb)
1101
+
1102
+ // append three dummy transforms to the tlist so that
1103
+ // we can translate,scale,translate in mousemove
1104
+ const pos = getRotationAngle(mouseTarget) ? 1 : 0
1105
+
1106
+ if (hasMatrixTransform(tlist)) {
1107
+ tlist.insertItemBefore(svgRoot.createSVGTransform(), pos)
1108
+ tlist.insertItemBefore(svgRoot.createSVGTransform(), pos)
1109
+ tlist.insertItemBefore(svgRoot.createSVGTransform(), pos)
1110
+ } else {
1111
+ tlist.appendItem(svgRoot.createSVGTransform())
1112
+ tlist.appendItem(svgRoot.createSVGTransform())
1113
+ tlist.appendItem(svgRoot.createSVGTransform())
1114
+ }
1115
+ break
1116
+ }
1117
+ case 'fhellipse':
1118
+ case 'fhrect':
1119
+ case 'fhpath':
1120
+ svgCanvas.setStart({ x: realX, y: realY })
1121
+ svgCanvas.setControllPoint1('x', 0)
1122
+ svgCanvas.setControllPoint1('y', 0)
1123
+ svgCanvas.setControllPoint2('x', 0)
1124
+ svgCanvas.setControllPoint2('y', 0)
1125
+ svgCanvas.setStarted(true)
1126
+ svgCanvas.setDAttr(realX + ',' + realY + ' ')
1127
+ // Commented out as doing nothing now:
1128
+ // strokeW = parseFloat(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;
1129
+ svgCanvas.addSVGElementsFromJson({
1130
+ element: 'polyline',
1131
+ curStyles: true,
1132
+ attr: {
1133
+ points: svgCanvas.getDAttr(),
1134
+ id: svgCanvas.getNextId(),
1135
+ fill: 'none',
1136
+ opacity: curShape.opacity / 2,
1137
+ 'stroke-linecap': 'round',
1138
+ style: 'pointer-events:none'
1139
+ }
1140
+ })
1141
+ svgCanvas.setFreehand('minx', realX)
1142
+ svgCanvas.setFreehand('maxx', realX)
1143
+ svgCanvas.setFreehand('miny', realY)
1144
+ svgCanvas.setFreehand('maxy', realY)
1145
+ break
1146
+ case 'image': {
1147
+ svgCanvas.setStarted(true)
1148
+ const newImage = svgCanvas.addSVGElementsFromJson({
1149
+ element: 'image',
1150
+ attr: {
1151
+ x,
1152
+ y,
1153
+ width: 0,
1154
+ height: 0,
1155
+ id: svgCanvas.getNextId(),
1156
+ opacity: curShape.opacity / 2,
1157
+ style: 'pointer-events:inherit'
1158
+ }
1159
+ })
1160
+ setHref(newImage, svgCanvas.getLastGoodImgUrl())
1161
+ preventClickDefault(newImage)
1162
+ break
1163
+ } case 'square':
1164
+ // TODO: once we create the rect, we lose information that this was a square
1165
+ // (for resizing purposes this could be important)
1166
+ // Fallthrough
1167
+ case 'rect':
1168
+ svgCanvas.setStarted(true)
1169
+ svgCanvas.setStartX(x)
1170
+ svgCanvas.setStartY(y)
1171
+ svgCanvas.addSVGElementsFromJson({
1172
+ element: 'rect',
1173
+ curStyles: true,
1174
+ attr: {
1175
+ x,
1176
+ y,
1177
+ width: 0,
1178
+ height: 0,
1179
+ id: svgCanvas.getNextId(),
1180
+ opacity: curShape.opacity / 2
1181
+ }
1182
+ })
1183
+ break
1184
+ case 'line': {
1185
+ svgCanvas.setStarted(true)
1186
+ const strokeW = Number(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width
1187
+ svgCanvas.addSVGElementsFromJson({
1188
+ element: 'line',
1189
+ curStyles: true,
1190
+ attr: {
1191
+ x1: x,
1192
+ y1: y,
1193
+ x2: x,
1194
+ y2: y,
1195
+ id: svgCanvas.getNextId(),
1196
+ stroke: curShape.stroke,
1197
+ 'stroke-width': strokeW,
1198
+ 'stroke-dasharray': curShape.stroke_dasharray,
1199
+ 'stroke-linejoin': curShape.stroke_linejoin,
1200
+ 'stroke-linecap': curShape.stroke_linecap,
1201
+ 'stroke-opacity': curShape.stroke_opacity,
1202
+ fill: 'none',
1203
+ opacity: curShape.opacity / 2,
1204
+ style: 'pointer-events:none'
1205
+ }
1206
+ })
1207
+ break
1208
+ } case 'circle':
1209
+ svgCanvas.setStarted(true)
1210
+ svgCanvas.addSVGElementsFromJson({
1211
+ element: 'circle',
1212
+ curStyles: true,
1213
+ attr: {
1214
+ cx: x,
1215
+ cy: y,
1216
+ r: 0,
1217
+ id: svgCanvas.getNextId(),
1218
+ opacity: curShape.opacity / 2
1219
+ }
1220
+ })
1221
+ break
1222
+ case 'ellipse':
1223
+ svgCanvas.setStarted(true)
1224
+ svgCanvas.addSVGElementsFromJson({
1225
+ element: 'ellipse',
1226
+ curStyles: true,
1227
+ attr: {
1228
+ cx: x,
1229
+ cy: y,
1230
+ rx: 0,
1231
+ ry: 0,
1232
+ id: svgCanvas.getNextId(),
1233
+ opacity: curShape.opacity / 2
1234
+ }
1235
+ })
1236
+ break
1237
+ case 'text':
1238
+ svgCanvas.setStarted(true)
1239
+ /* const newText = */ svgCanvas.addSVGElementsFromJson({
1240
+ element: 'text',
1241
+ curStyles: true,
1242
+ attr: {
1243
+ x,
1244
+ y,
1245
+ id: svgCanvas.getNextId(),
1246
+ fill: svgCanvas.getCurText('fill'),
1247
+ 'stroke-width': svgCanvas.getCurText('stroke_width'),
1248
+ 'font-size': svgCanvas.getCurText('font_size'),
1249
+ 'font-family': svgCanvas.getCurText('font_family'),
1250
+ 'text-anchor': 'middle',
1251
+ 'xml:space': 'preserve',
1252
+ opacity: curShape.opacity
1253
+ }
1254
+ })
1255
+ // newText.textContent = 'text';
1256
+ break
1257
+ case 'path':
1258
+ // Fall through
1259
+ case 'pathedit':
1260
+ svgCanvas.setStartX(svgCanvas.getStartX() * zoom)
1261
+ svgCanvas.setStartY(svgCanvas.getStartY() * zoom)
1262
+ svgCanvas.pathActions.mouseDown(evt, mouseTarget, svgCanvas.getStartX(), svgCanvas.getStartY())
1263
+ svgCanvas.setStarted(true)
1264
+ break
1265
+ case 'textedit':
1266
+ svgCanvas.setStartX(svgCanvas.getStartX() * zoom)
1267
+ svgCanvas.setStartY(svgCanvas.getStartY() * zoom)
1268
+ svgCanvas.textActions.mouseDown(evt, mouseTarget, svgCanvas.getStartX(), svgCanvas.getStartY())
1269
+ svgCanvas.setStarted(true)
1270
+ break
1271
+ case 'rotate':
1272
+ svgCanvas.setStarted(true)
1273
+ // we are starting an undoable change (a drag-rotation)
1274
+ svgCanvas.undoMgr.beginUndoableChange('transform', selectedElements)
1275
+ break
1276
+ default:
1277
+ // This could occur in an extension
1278
+ break
1279
+ }
1280
+
1281
+ /**
1282
+ * The main (left) mouse button is held down on the canvas area.
1283
+ * @event module:svgcanvas.SvgCanvas#event:ext_mouseDown
1284
+ * @type {PlainObject}
1285
+ * @property {MouseEvent} event The event object
1286
+ * @property {Float} start_x x coordinate on canvas
1287
+ * @property {Float} start_y y coordinate on canvas
1288
+ * @property {Element[]} selectedElements An array of the selected Elements
1289
+ */
1290
+ const extResult = svgCanvas.runExtensions('mouseDown', {
1291
+ event: evt,
1292
+ start_x: svgCanvas.getStartX(),
1293
+ start_y: svgCanvas.getStartY(),
1294
+ selectedElements
1295
+ }, true)
1296
+
1297
+ extResult.forEach((r) => {
1298
+ if (r?.started) {
1299
+ svgCanvas.setStarted(true)
1300
+ }
1301
+ })
1302
+ }
1303
+ /**
1304
+ * @param {Event} e
1305
+ * @fires module:event.SvgCanvas#event:updateCanvas
1306
+ * @fires module:event.SvgCanvas#event:zoomDone
1307
+ * @returns {void}
1308
+ */
1309
+ const DOMMouseScrollEvent = (e) => {
1310
+ const zoom = svgCanvas.getZoom()
1311
+ const { $id } = svgCanvas
1312
+ if (!e.shiftKey) { return }
1313
+
1314
+ e.preventDefault()
1315
+
1316
+ svgCanvas.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse())
1317
+
1318
+ const workarea = document.getElementById('workarea')
1319
+ const scrbar = 15
1320
+ const rulerwidth = svgCanvas.getCurConfig().showRulers ? 16 : 0
1321
+
1322
+ // mouse relative to content area in content pixels
1323
+ const pt = transformPoint(e.clientX, e.clientY, svgCanvas.getrootSctm())
1324
+
1325
+ // full work area width in screen pixels
1326
+ const editorFullW = parseFloat(getComputedStyle(workarea, null).width.replace('px', ''))
1327
+ const editorFullH = parseFloat(getComputedStyle(workarea, null).height.replace('px', ''))
1328
+
1329
+ // work area width minus scroll and ruler in screen pixels
1330
+ const editorW = editorFullW - scrbar - rulerwidth
1331
+ const editorH = editorFullH - scrbar - rulerwidth
1332
+
1333
+ // work area width in content pixels
1334
+ const workareaViewW = editorW * svgCanvas.getrootSctm().a
1335
+ const workareaViewH = editorH * svgCanvas.getrootSctm().d
1336
+
1337
+ // content offset from canvas in screen pixels
1338
+ const wOffset = findPos(workarea)
1339
+ const wOffsetLeft = wOffset.left + rulerwidth
1340
+ const wOffsetTop = wOffset.top + rulerwidth
1341
+
1342
+ const delta = (e.wheelDelta) ? e.wheelDelta : (e.detail) ? -e.detail : 0
1343
+ if (!delta) { return }
1344
+
1345
+ let factor = Math.max(3 / 4, Math.min(4 / 3, (delta)))
1346
+
1347
+ let wZoom; let hZoom
1348
+ if (factor > 1) {
1349
+ wZoom = Math.ceil(editorW / workareaViewW * factor * 100) / 100
1350
+ hZoom = Math.ceil(editorH / workareaViewH * factor * 100) / 100
1351
+ } else {
1352
+ wZoom = Math.floor(editorW / workareaViewW * factor * 100) / 100
1353
+ hZoom = Math.floor(editorH / workareaViewH * factor * 100) / 100
1354
+ }
1355
+ let zoomlevel = Math.min(wZoom, hZoom)
1356
+ zoomlevel = Math.min(10, Math.max(0.01, zoomlevel))
1357
+ if (zoomlevel === zoom) {
1358
+ return
1359
+ }
1360
+ factor = zoomlevel / zoom
1361
+
1362
+ // top left of workarea in content pixels before zoom
1363
+ const topLeftOld = transformPoint(wOffsetLeft, wOffsetTop, svgCanvas.getrootSctm())
1364
+
1365
+ // top left of workarea in content pixels after zoom
1366
+ const topLeftNew = {
1367
+ x: pt.x - (pt.x - topLeftOld.x) / factor,
1368
+ y: pt.y - (pt.y - topLeftOld.y) / factor
1369
+ }
1370
+
1371
+ // top left of workarea in canvas pixels relative to content after zoom
1372
+ const topLeftNewCanvas = {
1373
+ x: topLeftNew.x * zoomlevel,
1374
+ y: topLeftNew.y * zoomlevel
1375
+ }
1376
+
1377
+ // new center in canvas pixels
1378
+ const newCtr = {
1379
+ x: topLeftNewCanvas.x - rulerwidth + editorFullW / 2,
1380
+ y: topLeftNewCanvas.y - rulerwidth + editorFullH / 2
1381
+ }
1382
+
1383
+ svgCanvas.setZoom(zoomlevel)
1384
+ document.getElementById('zoom').value = ((zoomlevel * 100).toFixed(1))
1385
+
1386
+ svgCanvas.call('updateCanvas', { center: false, newCtr })
1387
+ svgCanvas.call('zoomDone')
1388
+ }