UNPKG

svgedit

Version:

Powerful SVG-Editor for your browser

330 lines (307 loc) 11.6 kB
/** * @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) } } } } }