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