-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
ext-markers.js
329 lines (307 loc) · 11.3 KB
/
ext-markers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/**
* @file ext-markers.js
*
* @license Apache-2.0
*
* @copyright 2010 Will Schleter based on ext-arrows.js by Copyright(c) 2010 Alexis Deveria
* @copyright 2021 OptimistikSAS
*
* This extension provides for the addition of markers to the either end
* or the middle of a line, polyline, path, polygon.
*
* Markers are graphics
*
* to simplify the coding and make the implementation as robust as possible,
* markers are not shared - every object has its own set of markers.
* this relationship is maintained by a naming convention between the
* ids of the markers and the ids of the object
*
* The following restrictions exist for simplicty of use and programming
* objects and their markers to have the same color
* marker size is fixed
* an application specific attribute - se_type - is added to each marker element
* to store the type of marker
*
* @todo
* remove some of the restrictions above
*
*/
export default {
name: 'markers',
async init () {
const svgEditor = this
const { svgCanvas } = svgEditor
const { BatchCommand, RemoveElementCommand, InsertElementCommand } = svgCanvas.history
const { $id, addSVGElementsFromJson: addElem } = svgCanvas
const mtypes = ['start', 'mid', 'end']
const markerElems = ['line', 'path', 'polyline', 'polygon']
// note - to add additional marker types add them below with a unique id
// and add the associated icon(s) to marker-icons.svg
// the geometry is normalized to a 100x100 box with the origin at lower left
// Safari did not like negative values for low left of viewBox
// remember that the coordinate system has +y downward
const markerTypes = {
nomarker: {},
leftarrow:
{ element: 'path', attr: { d: 'M0,50 L100,90 L70,50 L100,10 Z' } },
rightarrow:
{ element: 'path', attr: { d: 'M100,50 L0,90 L30,50 L0,10 Z' } },
box:
{ element: 'path', attr: { d: 'M20,20 L20,80 L80,80 L80,20 Z' } },
mcircle:
{ element: 'circle', attr: { r: 30, cx: 50, cy: 50 } }
};
// duplicate shapes to support unfilled (open) marker types with an _o suffix
['leftarrow', 'rightarrow', 'box', 'mcircle'].forEach((v) => {
markerTypes[v + '_o'] = markerTypes[v]
})
/**
* @param {Element} elem - A graphic element will have an attribute like marker-start
* @param {"marker-start"|"marker-mid"|"marker-end"} attr
* @returns {Element} The marker element that is linked to the graphic element
*/
const getLinked = (elem, attr) => {
const str = elem.getAttribute(attr)
if (!str) { return null }
const m = str.match(/\(#(.*)\)/)
// "url(#mkr_end_svg_1)" would give m[1] = "mkr_end_svg_1"
if (!m || m.length !== 2) {
return null
}
return svgCanvas.getElement(m[1])
}
/**
* Toggles context tool panel off/on.
* @param {boolean} on
* @returns {void}
*/
const showPanel = (on, elem) => {
$id('marker_panel').style.display = (on) ? 'block' : 'none'
if (on && elem) {
mtypes.forEach((pos) => {
const marker = getLinked(elem, 'marker-' + pos)
if (marker?.attributes?.se_type) {
$id(`${pos}_marker_list_opts`).setAttribute('value', marker.attributes.se_type.value)
} else {
$id(`${pos}_marker_list_opts`).setAttribute('value', 'nomarker')
}
})
}
}
/**
* @param {string} id
* @param {""|"nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} seType
* @returns {SVGMarkerElement}
*/
const addMarker = (id, seType) => {
const selElems = svgCanvas.getSelectedElements()
let marker = svgCanvas.getElement(id)
if (marker) { return undefined }
if (seType === '' || seType === 'nomarker') { return undefined }
const el = selElems[0]
const color = el.getAttribute('stroke')
const strokeWidth = 10
const refX = 50
const refY = 50
const viewBox = '0 0 100 100'
const markerWidth = 5
const markerHeight = 5
if (!markerTypes[seType]) {
console.error(`unknown marker type: ${seType}`)
return undefined
}
// create a generic marker
marker = addElem({
element: 'marker',
attr: {
id,
markerUnits: 'strokeWidth',
orient: 'auto',
style: 'pointer-events:none',
se_type: seType
}
})
const mel = addElem(markerTypes[seType])
const fillcolor = (seType.substr(-2) === '_o')
? 'none'
: color
mel.setAttribute('fill', fillcolor)
mel.setAttribute('stroke', color)
mel.setAttribute('stroke-width', strokeWidth)
marker.append(mel)
marker.setAttribute('viewBox', viewBox)
marker.setAttribute('markerWidth', markerWidth)
marker.setAttribute('markerHeight', markerHeight)
marker.setAttribute('refX', refX)
marker.setAttribute('refY', refY)
svgCanvas.findDefs().append(marker)
return marker
}
/**
* @param {Element} elem
* @returns {SVGPolylineElement}
*/
const convertline = (elem) => {
// this routine came from the connectors extension
// it is needed because midpoint markers don't work with line elements
if (elem.tagName !== 'line') { return elem }
// Convert to polyline to accept mid-arrow
const x1 = Number(elem.getAttribute('x1'))
const x2 = Number(elem.getAttribute('x2'))
const y1 = Number(elem.getAttribute('y1'))
const y2 = Number(elem.getAttribute('y2'))
const { id } = elem
const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' ')
const pline = addElem({
element: 'polyline',
attr: {
points: (x1 + ',' + y1 + midPt + x2 + ',' + y2),
stroke: elem.getAttribute('stroke'),
'stroke-width': elem.getAttribute('stroke-width'),
fill: 'none',
opacity: elem.getAttribute('opacity') || 1
}
})
mtypes.forEach((pos) => { // get any existing marker definitions
const nam = 'marker-' + pos
const m = elem.getAttribute(nam)
if (m) { pline.setAttribute(nam, elem.getAttribute(nam)) }
})
const batchCmd = new BatchCommand()
batchCmd.addSubCommand(new RemoveElementCommand(elem, elem.parentNode))
batchCmd.addSubCommand(new InsertElementCommand(pline))
elem.insertAdjacentElement('afterend', pline)
elem.remove()
svgCanvas.clearSelection()
pline.id = id
svgCanvas.addToSelection([pline])
svgCanvas.addCommandToHistory(batchCmd)
return pline
}
/**
*
* @returns {void}
*/
const setMarker = (pos, markerType) => {
const selElems = svgCanvas.getSelectedElements()
if (selElems.length === 0) return
const markerName = 'marker-' + pos
const el = selElems[0]
const marker = getLinked(el, markerName)
if (marker) { marker.remove() }
el.removeAttribute(markerName)
let val = markerType
if (val === '') { val = 'nomarker' }
if (val === 'nomarker') {
svgCanvas.call('changed', selElems)
return
}
// Set marker on element
const id = 'mkr_' + pos + '_' + el.id
addMarker(id, val)
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')')
if (el.tagName === 'line' && pos === 'mid') {
convertline(el)
}
svgCanvas.call('changed', selElems)
}
/**
* Called when the main system modifies an object. This routine changes
* the associated markers to be the same color.
* @param {Element} elem
* @returns {void}
*/
const colorChanged = (elem) => {
const color = elem.getAttribute('stroke')
mtypes.forEach((pos) => {
const marker = getLinked(elem, 'marker-' + pos)
if (!marker) { return }
if (!marker.attributes.se_type) { return } // not created by this extension
const ch = marker.lastElementChild
if (!ch) { return }
const curfill = ch.getAttribute('fill')
const curstroke = ch.getAttribute('stroke')
if (curfill && curfill !== 'none') { ch.setAttribute('fill', color) }
if (curstroke && curstroke !== 'none') { ch.setAttribute('stroke', color) }
})
}
/**
* Called when the main system creates or modifies an object.
* Its primary purpose is to create new markers for cloned objects.
* @param {Element} el
* @returns {void}
*/
const updateReferences = (el) => {
const selElems = svgCanvas.getSelectedElements()
mtypes.forEach((pos) => {
const markerName = 'marker-' + pos
const marker = getLinked(el, markerName)
if (!marker || !marker.attributes.se_type) { return } // not created by this extension
const url = el.getAttribute(markerName)
if (url) {
const len = el.id.length
const linkid = url.substr(-len - 1, len)
if (el.id !== linkid) {
const newMarkerId = 'mkr_' + pos + '_' + el.id
addMarker(newMarkerId, marker.attributes.se_type.value)
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + newMarkerId + ')')
svgCanvas.call('changed', selElems)
}
}
})
}
return {
name: svgEditor.i18next.t(`${name}:name`),
// The callback should be used to load the DOM with the appropriate UI items
callback () {
// Add the context panel and its handler(s)
const panelTemplate = document.createElement('template')
// create the marker panel
let innerHTML = '<div id="marker_panel">'
mtypes.forEach((pos) => {
innerHTML += `<se-list id="${pos}_marker_list_opts" title="tools.${pos}_marker_list_opts" label="" width="22px" height="22px">`
Object.entries(markerTypes).forEach(([marker, _mkr]) => {
innerHTML += `<se-list-item id="mkr_${pos}_${marker}" value="${marker}" title="tools.mkr_${marker}" src="${marker}.svg" img-height="22px"></se-list-item>`
})
innerHTML += '</se-list>'
})
innerHTML += '</div>'
panelTemplate.innerHTML = innerHTML
$id('tools_top').appendChild(panelTemplate.content.cloneNode(true))
// don't display the panels on start
showPanel(false)
mtypes.forEach((pos) => {
$id(`${pos}_marker_list_opts`).addEventListener('change', (evt) => {
setMarker(pos, evt.detail.value)
})
})
},
selectedChanged (opts) {
// Use this to update the current selected elements
if (opts.elems.length === 0) showPanel(false)
opts.elems.forEach((elem) => {
if (elem && markerElems.includes(elem.tagName)) {
if (opts.selectedElement && !opts.multiselected) {
showPanel(true, elem)
} else {
showPanel(false)
}
} else {
showPanel(false)
}
})
},
elementChanged (opts) {
const elem = opts.elems[0]
if (elem && (
elem.getAttribute('marker-start') ||
elem.getAttribute('marker-mid') ||
elem.getAttribute('marker-end')
)) {
colorChanged(elem)
updateReferences(elem)
}
}
}
}
}