restore markers

master
JFH 2021-10-17 13:43:33 +02:00
parent 28e8601efd
commit e723c71823
6 changed files with 434 additions and 6 deletions

View File

@ -175,8 +175,7 @@ export default class ConfigObj {
'ext-eyedropper',
'ext-grid',
'ext-imagelib',
// 'ext-arrows',
// 'ext-markers',
'ext-markers',
'ext-overview_window',
'ext-panning',
'ext-shapes',

View File

@ -0,0 +1,340 @@
/**
* @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
*
*/
const loadExtensionTranslation = async function (lang) {
let translationModule;
try {
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
};
export default {
name: 'markers',
async init (S) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const { svgCanvas } = svgEditor;
const { $id, addSVGElementFromJson: addElem } = svgCanvas;
const mtypes = [ 'start', 'mid', 'end' ];
// 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(/\(#(.*)\)/);
if (!m || m.length !== 2) {
return null;
}
return svgCanvas.getElem(m[1]);
};
let selElems;
/**
* Toggles context tool panel off/on.
* @param {boolean} on
* @returns {void}
*/
const showPanel = (on) => {
$id('marker_panel').style.display = (on) ? 'block' : 'none';
};
/**
* @param {string} id
* @param {""|"\\nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} seType
* @returns {SVGMarkerElement}
*/
const addMarker = (id, seType) => {
let marker = svgCanvas.getElem(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 S.BatchCommand();
batchCmd.addSubCommand(new S.RemoveElementCommand(elem, elem.parentNode));
batchCmd.addSubCommand(new S.InsertElementCommand(pline));
elem.insertAdjacentElement('afterend', pline);
elem.remove();
svgCanvas.clearSelection();
pline.id = id;
svgCanvas.addToSelection([ pline ]);
S.addCommandToHistory(batchCmd);
return pline;
};
/**
*
* @returns {void}
*/
const setMarker = (pos, markerType) => {
if (!selElems) 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) => {
mtypes.forEach((pos) => {
const id = 'mkr_' + pos + '_' + el.id;
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 val = $id(pos + '_marker').getAttribute('value');
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
if (el.tagName === 'line' && pos === 'mid') { el = convertline(el); }
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");
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>';
// eslint-disable-next-line no-unsanitized/property
panelTemplate.innerHTML = innerHTML;
$id("tools_top").appendChild(panelTemplate.content.cloneNode(true));
mtypes.forEach((pos) => {
$id(`${pos}_marker_list_opts`).addEventListener('change', (evt) => {
setMarker(pos, evt.detail.value);
});
});
// don't display the panels on start
showPanel(false);
},
/* async */ addLangData ({ _importLocale, _lang }) {
return { data: strings.langList };
},
selectedChanged (opts) {
// Use this to update the current selected elements
selElems = opts.elems;
const markerElems = [ 'line', 'path', 'polyline', 'polygon' ];
let i = selElems.length;
while (i--) {
const elem = selElems[i];
if (elem && markerElems.includes(elem.tagName)) {
if (opts.selectedElement && !opts.multiselected) {
showPanel(true);
} 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);
}
}
};
}
};

View File

@ -0,0 +1,46 @@
export default {
name: 'Markers',
langList: [
{ id: 'nomarker', title: 'No Marker' },
{ id: 'leftarrow', title: 'Left Arrow' },
{ id: 'rightarrow', title: 'Right Arrow' },
{ id: 'textmarker', title: 'Text Marker' },
{ id: 'forwardslash', title: 'Forward Slash' },
{ id: 'reverseslash', title: 'Reverse Slash' },
{ id: 'verticalslash', title: 'Vertical Slash' },
{ id: 'box', title: 'Box' },
{ id: 'star', title: 'Star' },
{ id: 'xmark', title: 'X' },
{ id: 'triangle', title: 'Triangle' },
{ id: 'mcircle', title: 'Circle' },
{ id: 'leftarrow_o', title: 'Open Left Arrow' },
{ id: 'rightarrow_o', title: 'Open Right Arrow' },
{ id: 'box_o', title: 'Open Box' },
{ id: 'star_o', title: 'Open Star' },
{ id: 'triangle_o', title: 'Open Triangle' },
{ id: 'mcircle_o', title: 'Open Circle' }
],
contextTools: [
{
title: 'Start marker',
label: 's'
},
{
title: 'Select start marker type'
},
{
title: 'Middle marker',
label: 'm'
},
{
title: 'Select mid marker type'
},
{
title: 'End marker',
label: 'e'
},
{
title: 'Select end marker type'
}
]
};

View File

@ -0,0 +1,46 @@
export default {
name: '标记',
langList: [
{ id: 'nomarker', title: '无标记' },
{ id: 'leftarrow', title: '左箭头' },
{ id: 'rightarrow', title: '右箭头' },
{ id: 'textmarker', title: '文本' },
{ id: 'forwardslash', title: '斜杠' },
{ id: 'reverseslash', title: '反斜杠' },
{ id: 'verticalslash', title: '垂直线' },
{ id: 'box', title: '方块' },
{ id: 'star', title: '星形' },
{ id: 'xmark', title: 'X' },
{ id: 'triangle', title: '三角形' },
{ id: 'mcircle', title: '圆形' },
{ id: 'leftarrow_o', title: '左箭头(空心)' },
{ id: 'rightarrow_o', title: '右箭头(空心)' },
{ id: 'box_o', title: '方块(空心)' },
{ id: 'star_o', title: '星形(空心)' },
{ id: 'triangle_o', title: '三角形(空心)' },
{ id: 'mcircle_o', title: '圆形(空心)' }
],
contextTools: [
{
title: '起始标记',
label: 's'
},
{
title: '选择起始标记类型'
},
{
title: '中段标记',
label: 'm'
},
{
title: '选择中段标记类型'
},
{
title: '末端标记',
label: 'e'
},
{
title: '选择末端标记类型'
}
]
};

View File

@ -449,9 +449,6 @@ export default {
showPanel(false, "polygon");
}
}
},
elementChanged(_opts) {
// const elem = opts.elems[0];
}
};
}

View File

@ -42,7 +42,7 @@ const svgWhiteList_ = {
image: [ 'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'systemLanguage', 'width', 'x', 'xlink:href', 'xlink:title', 'y' ],
line: [ 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'x1', 'x2', 'y1', 'y2' ],
linearGradient: [ 'gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2' ],
marker: [ 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'systemLanguage', 'viewBox' ],
marker: [ 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'se_type', 'systemLanguage', 'viewBox' ],
mask: [ 'height', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y' ],
metadata: [ ],
path: [ 'clip-path', 'clip-rule', 'd', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage' ],