diff --git a/coverage/coverage-summary.json b/coverage/coverage-summary.json
index 4a45cfe53..cf04a894a 100644
--- a/coverage/coverage-summary.json
+++ b/coverage/coverage-summary.json
@@ -1,69 +1,71 @@
-{"total": {"lines":{"total":6260,"covered":3796,"skipped":0,"pct":60.64},"statements":{"total":6574,"covered":3913,"skipped":0,"pct":59.52},"functions":{"total":949,"covered":495,"skipped":0,"pct":52.16},"branches":{"total":3223,"covered":1398,"skipped":0,"pct":43.38}}
-,"/Users/jfhenon/Work/svgedit/packages/svgcanvas/common/browser.js": {"lines":{"total":25,"covered":24,"skipped":0,"pct":96},"functions":{"total":6,"covered":2,"skipped":0,"pct":33.33},"statements":{"total":30,"covered":25,"skipped":0,"pct":83.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/packages/svgcanvas/common/util.js": {"lines":{"total":90,"covered":8,"skipped":0,"pct":8.89},"functions":{"total":7,"covered":3,"skipped":0,"pct":42.86},"statements":{"total":92,"covered":10,"skipped":0,"pct":10.87},"branches":{"total":98,"covered":10,"skipped":0,"pct":10.2}}
-,"/Users/jfhenon/Work/svgedit/src/editor/ConfigObj.js": {"lines":{"total":101,"covered":46,"skipped":0,"pct":45.54},"functions":{"total":14,"covered":11,"skipped":0,"pct":78.57},"statements":{"total":102,"covered":46,"skipped":0,"pct":45.1},"branches":{"total":96,"covered":30,"skipped":0,"pct":31.25}}
-,"/Users/jfhenon/Work/svgedit/src/editor/Editor.js": {"lines":{"total":410,"covered":200,"skipped":0,"pct":48.78},"functions":{"total":102,"covered":34,"skipped":0,"pct":33.33},"statements":{"total":417,"covered":201,"skipped":0,"pct":48.2},"branches":{"total":213,"covered":78,"skipped":0,"pct":36.62}}
-,"/Users/jfhenon/Work/svgedit/src/editor/EditorStartup.js": {"lines":{"total":346,"covered":216,"skipped":0,"pct":62.43},"functions":{"total":51,"covered":27,"skipped":0,"pct":52.94},"statements":{"total":357,"covered":224,"skipped":0,"pct":62.75},"branches":{"total":122,"covered":41,"skipped":0,"pct":33.61}}
-,"/Users/jfhenon/Work/svgedit/src/editor/MainMenu.js": {"lines":{"total":111,"covered":44,"skipped":0,"pct":39.64},"functions":{"total":14,"covered":7,"skipped":0,"pct":50},"statements":{"total":111,"covered":44,"skipped":0,"pct":39.64},"branches":{"total":48,"covered":7,"skipped":0,"pct":14.58}}
-,"/Users/jfhenon/Work/svgedit/src/editor/Rulers.js": {"lines":{"total":119,"covered":93,"skipped":0,"pct":78.15},"functions":{"total":6,"covered":5,"skipped":0,"pct":83.33},"statements":{"total":124,"covered":98,"skipped":0,"pct":79.03},"branches":{"total":43,"covered":32,"skipped":0,"pct":74.42}}
-,"/Users/jfhenon/Work/svgedit/src/editor/browser-not-supported.js": {"lines":{"total":4,"covered":3,"skipped":0,"pct":75},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":3,"skipped":0,"pct":75},"branches":{"total":4,"covered":3,"skipped":0,"pct":75}}
-,"/Users/jfhenon/Work/svgedit/src/editor/contextmenu.js": {"lines":{"total":22,"covered":9,"skipped":0,"pct":40.91},"functions":{"total":8,"covered":1,"skipped":0,"pct":12.5},"statements":{"total":23,"covered":9,"skipped":0,"pct":39.13},"branches":{"total":13,"covered":0,"skipped":0,"pct":0}}
-,"/Users/jfhenon/Work/svgedit/src/editor/locale.js": {"lines":{"total":14,"covered":9,"skipped":0,"pct":64.29},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":14,"covered":9,"skipped":0,"pct":64.29},"branches":{"total":10,"covered":3,"skipped":0,"pct":30}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/PaintBox.js": {"lines":{"total":64,"covered":51,"skipped":0,"pct":79.69},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":67,"covered":53,"skipped":0,"pct":79.1},"branches":{"total":33,"covered":21,"skipped":0,"pct":63.64}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/index.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seButton.js": {"lines":{"total":57,"covered":40,"skipped":0,"pct":70.18},"functions":{"total":15,"covered":7,"skipped":0,"pct":46.67},"statements":{"total":60,"covered":42,"skipped":0,"pct":70},"branches":{"total":33,"covered":23,"skipped":0,"pct":69.7}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seColorPicker.js": {"lines":{"total":50,"covered":41,"skipped":0,"pct":82},"functions":{"total":16,"covered":11,"skipped":0,"pct":68.75},"statements":{"total":51,"covered":41,"skipped":0,"pct":80.39},"branches":{"total":11,"covered":8,"skipped":0,"pct":72.73}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seExplorerButton.js": {"lines":{"total":95,"covered":72,"skipped":0,"pct":75.79},"functions":{"total":17,"covered":10,"skipped":0,"pct":58.82},"statements":{"total":98,"covered":72,"skipped":0,"pct":73.47},"branches":{"total":30,"covered":17,"skipped":0,"pct":56.67}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seFlyingButton.js": {"lines":{"total":74,"covered":47,"skipped":0,"pct":63.51},"functions":{"total":14,"covered":9,"skipped":0,"pct":64.29},"statements":{"total":75,"covered":48,"skipped":0,"pct":64},"branches":{"total":29,"covered":12,"skipped":0,"pct":41.38}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seInput.js": {"lines":{"total":46,"covered":31,"skipped":0,"pct":67.39},"functions":{"total":16,"covered":8,"skipped":0,"pct":50},"statements":{"total":47,"covered":31,"skipped":0,"pct":65.96},"branches":{"total":8,"covered":4,"skipped":0,"pct":50}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seList.js": {"lines":{"total":87,"covered":59,"skipped":0,"pct":67.82},"functions":{"total":21,"covered":10,"skipped":0,"pct":47.62},"statements":{"total":89,"covered":61,"skipped":0,"pct":68.54},"branches":{"total":22,"covered":12,"skipped":0,"pct":54.55}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seListItem.js": {"lines":{"total":39,"covered":26,"skipped":0,"pct":66.67},"functions":{"total":12,"covered":4,"skipped":0,"pct":33.33},"statements":{"total":40,"covered":27,"skipped":0,"pct":67.5},"branches":{"total":10,"covered":8,"skipped":0,"pct":80}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seMenu.js": {"lines":{"total":26,"covered":20,"skipped":0,"pct":76.92},"functions":{"total":7,"covered":3,"skipped":0,"pct":42.86},"statements":{"total":27,"covered":20,"skipped":0,"pct":74.07},"branches":{"total":5,"covered":3,"skipped":0,"pct":60}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seMenuItem.js": {"lines":{"total":37,"covered":26,"skipped":0,"pct":70.27},"functions":{"total":9,"covered":5,"skipped":0,"pct":55.56},"statements":{"total":40,"covered":27,"skipped":0,"pct":67.5},"branches":{"total":19,"covered":8,"skipped":0,"pct":42.11}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/sePalette.js": {"lines":{"total":33,"covered":32,"skipped":0,"pct":96.97},"functions":{"total":7,"covered":7,"skipped":0,"pct":100},"statements":{"total":33,"covered":32,"skipped":0,"pct":96.97},"branches":{"total":13,"covered":10,"skipped":0,"pct":76.92}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/sePlainBorderButton.js": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/sePlainMenuButton.js": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seSelect.js": {"lines":{"total":56,"covered":45,"skipped":0,"pct":80.36},"functions":{"total":17,"covered":9,"skipped":0,"pct":52.94},"statements":{"total":59,"covered":47,"skipped":0,"pct":79.66},"branches":{"total":16,"covered":14,"skipped":0,"pct":87.5}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seSpinInput.js": {"lines":{"total":65,"covered":55,"skipped":0,"pct":84.62},"functions":{"total":18,"covered":10,"skipped":0,"pct":55.56},"statements":{"total":66,"covered":55,"skipped":0,"pct":83.33},"branches":{"total":17,"covered":13,"skipped":0,"pct":76.47}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seText.js": {"lines":{"total":28,"covered":18,"skipped":0,"pct":64.29},"functions":{"total":10,"covered":4,"skipped":0,"pct":40},"statements":{"total":29,"covered":18,"skipped":0,"pct":62.07},"branches":{"total":8,"covered":5,"skipped":0,"pct":62.5}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/seZoom.js": {"lines":{"total":99,"covered":93,"skipped":0,"pct":93.94},"functions":{"total":28,"covered":24,"skipped":0,"pct":85.71},"statements":{"total":107,"covered":100,"skipped":0,"pct":93.46},"branches":{"total":36,"covered":21,"skipped":0,"pct":58.33}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/jgraduate/ColorValuePicker.js": {"lines":{"total":231,"covered":54,"skipped":0,"pct":23.38},"functions":{"total":9,"covered":3,"skipped":0,"pct":33.33},"statements":{"total":249,"covered":63,"skipped":0,"pct":25.3},"branches":{"total":157,"covered":34,"skipped":0,"pct":21.66}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/jgraduate/Slider.js": {"lines":{"total":156,"covered":91,"skipped":0,"pct":58.33},"functions":{"total":17,"covered":9,"skipped":0,"pct":52.94},"statements":{"total":176,"covered":96,"skipped":0,"pct":54.55},"branches":{"total":186,"covered":97,"skipped":0,"pct":52.15}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/jgraduate/jQuery.jGraduate.js": {"lines":{"total":580,"covered":275,"skipped":0,"pct":47.41},"functions":{"total":44,"covered":14,"skipped":0,"pct":31.82},"statements":{"total":602,"covered":282,"skipped":0,"pct":46.84},"branches":{"total":278,"covered":100,"skipped":0,"pct":35.97}}
-,"/Users/jfhenon/Work/svgedit/src/editor/components/jgraduate/jQuery.jPicker.js": {"lines":{"total":840,"covered":449,"skipped":0,"pct":53.45},"functions":{"total":60,"covered":39,"skipped":0,"pct":65},"statements":{"total":926,"covered":475,"skipped":0,"pct":51.3},"branches":{"total":777,"covered":327,"skipped":0,"pct":42.08}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/SePlainAlertDialog.js": {"lines":{"total":7,"covered":1,"skipped":0,"pct":14.29},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":7,"covered":1,"skipped":0,"pct":14.29},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/cmenuDialog.js": {"lines":{"total":120,"covered":112,"skipped":0,"pct":93.33},"functions":{"total":28,"covered":16,"skipped":0,"pct":57.14},"statements":{"total":131,"covered":117,"skipped":0,"pct":89.31},"branches":{"total":23,"covered":19,"skipped":0,"pct":82.61}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/cmenuLayersDialog.js": {"lines":{"total":61,"covered":49,"skipped":0,"pct":80.33},"functions":{"total":16,"covered":6,"skipped":0,"pct":37.5},"statements":{"total":66,"covered":49,"skipped":0,"pct":74.24},"branches":{"total":18,"covered":13,"skipped":0,"pct":72.22}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/editorPreferencesDialog.js": {"lines":{"total":157,"covered":125,"skipped":0,"pct":79.62},"functions":{"total":30,"covered":9,"skipped":0,"pct":30},"statements":{"total":159,"covered":126,"skipped":0,"pct":79.25},"branches":{"total":46,"covered":35,"skipped":0,"pct":76.09}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/exportDialog.js": {"lines":{"total":55,"covered":39,"skipped":0,"pct":70.91},"functions":{"total":14,"covered":5,"skipped":0,"pct":35.71},"statements":{"total":58,"covered":39,"skipped":0,"pct":67.24},"branches":{"total":12,"covered":6,"skipped":0,"pct":50}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/imagePropertiesDialog.js": {"lines":{"total":161,"covered":90,"skipped":0,"pct":55.9},"functions":{"total":20,"covered":5,"skipped":0,"pct":25},"statements":{"total":162,"covered":90,"skipped":0,"pct":55.56},"branches":{"total":53,"covered":23,"skipped":0,"pct":43.4}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/index.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/seAlertDialog.js": {"lines":{"total":6,"covered":2,"skipped":0,"pct":33.33},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":6,"covered":2,"skipped":0,"pct":33.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/seConfirmDialog.js": {"lines":{"total":8,"covered":2,"skipped":0,"pct":25},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":2,"skipped":0,"pct":25},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/sePromptDialog.js": {"lines":{"total":24,"covered":5,"skipped":0,"pct":20.83},"functions":{"total":7,"covered":2,"skipped":0,"pct":28.57},"statements":{"total":24,"covered":5,"skipped":0,"pct":20.83},"branches":{"total":9,"covered":0,"skipped":0,"pct":0}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/seSelectDialog.js": {"lines":{"total":8,"covered":2,"skipped":0,"pct":25},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":2,"skipped":0,"pct":25},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/svgSourceDialog.js": {"lines":{"total":74,"covered":56,"skipped":0,"pct":75.68},"functions":{"total":17,"covered":6,"skipped":0,"pct":35.29},"statements":{"total":75,"covered":57,"skipped":0,"pct":76},"branches":{"total":17,"covered":14,"skipped":0,"pct":82.35}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/se-elix/define/NumberSpinBox.js": {"lines":{"total":1,"covered":1,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":1,"covered":1,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js": {"lines":{"total":54,"covered":52,"skipped":0,"pct":96.3},"functions":{"total":13,"covered":11,"skipped":0,"pct":84.62},"statements":{"total":54,"covered":52,"skipped":0,"pct":96.3},"branches":{"total":53,"covered":47,"skipped":0,"pct":88.68}}
-,"/Users/jfhenon/Work/svgedit/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-eyedropper/ext-eyedropper.js": {"lines":{"total":60,"covered":39,"skipped":0,"pct":65},"functions":{"total":8,"covered":5,"skipped":0,"pct":62.5},"statements":{"total":71,"covered":39,"skipped":0,"pct":54.93},"branches":{"total":39,"covered":15,"skipped":0,"pct":38.46}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-eyedropper/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-grid/ext-grid.js": {"lines":{"total":88,"covered":43,"skipped":0,"pct":48.86},"functions":{"total":8,"covered":4,"skipped":0,"pct":50},"statements":{"total":90,"covered":43,"skipped":0,"pct":47.78},"branches":{"total":10,"covered":4,"skipped":0,"pct":40}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-grid/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-markers/ext-markers.js": {"lines":{"total":149,"covered":46,"skipped":0,"pct":30.87},"functions":{"total":21,"covered":12,"skipped":0,"pct":57.14},"statements":{"total":164,"covered":48,"skipped":0,"pct":29.27},"branches":{"total":80,"covered":22,"skipped":0,"pct":27.5}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-opensave/ext-opensave.js": {"lines":{"total":136,"covered":36,"skipped":0,"pct":26.47},"functions":{"total":13,"covered":3,"skipped":0,"pct":23.08},"statements":{"total":141,"covered":36,"skipped":0,"pct":25.53},"branches":{"total":32,"covered":0,"skipped":0,"pct":0}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-opensave/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-panning/ext-panning.js": {"lines":{"total":30,"covered":22,"skipped":0,"pct":73.33},"functions":{"total":7,"covered":6,"skipped":0,"pct":85.71},"statements":{"total":30,"covered":22,"skipped":0,"pct":73.33},"branches":{"total":6,"covered":2,"skipped":0,"pct":33.33}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-panning/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-polystar/ext-polystar.js": {"lines":{"total":247,"covered":232,"skipped":0,"pct":93.93},"functions":{"total":18,"covered":16,"skipped":0,"pct":88.89},"statements":{"total":256,"covered":241,"skipped":0,"pct":94.14},"branches":{"total":62,"covered":39,"skipped":0,"pct":62.9}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-polystar/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-shapes/ext-shapes.js": {"lines":{"total":76,"covered":74,"skipped":0,"pct":97.37},"functions":{"total":7,"covered":7,"skipped":0,"pct":100},"statements":{"total":80,"covered":78,"skipped":0,"pct":97.5},"branches":{"total":26,"covered":17,"skipped":0,"pct":65.38}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-shapes/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-storage/ext-storage.js": {"lines":{"total":91,"covered":59,"skipped":0,"pct":64.84},"functions":{"total":12,"covered":7,"skipped":0,"pct":58.33},"statements":{"total":91,"covered":59,"skipped":0,"pct":64.84},"branches":{"total":68,"covered":27,"skipped":0,"pct":39.71}}
-,"/Users/jfhenon/Work/svgedit/src/editor/extensions/ext-storage/storageDialog.js": {"lines":{"total":60,"covered":56,"skipped":0,"pct":93.33},"functions":{"total":10,"covered":7,"skipped":0,"pct":70},"statements":{"total":62,"covered":57,"skipped":0,"pct":91.94},"branches":{"total":15,"covered":13,"skipped":0,"pct":86.67}}
-,"/Users/jfhenon/Work/svgedit/src/editor/locale/lang.en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
-,"/Users/jfhenon/Work/svgedit/src/editor/panels/BottomPanel.js": {"lines":{"total":71,"covered":58,"skipped":0,"pct":81.69},"functions":{"total":22,"covered":14,"skipped":0,"pct":63.64},"statements":{"total":77,"covered":61,"skipped":0,"pct":79.22},"branches":{"total":30,"covered":16,"skipped":0,"pct":53.33}}
-,"/Users/jfhenon/Work/svgedit/src/editor/panels/LayersPanel.js": {"lines":{"total":157,"covered":70,"skipped":0,"pct":44.59},"functions":{"total":27,"covered":6,"skipped":0,"pct":22.22},"statements":{"total":163,"covered":71,"skipped":0,"pct":43.56},"branches":{"total":46,"covered":6,"skipped":0,"pct":13.04}}
-,"/Users/jfhenon/Work/svgedit/src/editor/panels/LeftPanel.js": {"lines":{"total":58,"covered":47,"skipped":0,"pct":81.03},"functions":{"total":20,"covered":15,"skipped":0,"pct":75},"statements":{"total":59,"covered":47,"skipped":0,"pct":79.66},"branches":{"total":32,"covered":12,"skipped":0,"pct":37.5}}
-,"/Users/jfhenon/Work/svgedit/src/editor/panels/TopPanel.js": {"lines":{"total":391,"covered":296,"skipped":0,"pct":75.7},"functions":{"total":73,"covered":48,"skipped":0,"pct":65.75},"statements":{"total":411,"covered":304,"skipped":0,"pct":73.97},"branches":{"total":207,"covered":127,"skipped":0,"pct":61.35}}
+{"total": {"lines":{"total":6564,"covered":3798,"skipped":0,"pct":57.86},"statements":{"total":6874,"covered":3901,"skipped":0,"pct":56.75},"functions":{"total":976,"covered":490,"skipped":0,"pct":50.2},"branches":{"total":3361,"covered":1358,"skipped":0,"pct":40.4}}
+,"/Users/jean-francoishenon/Work/svgedit/packages/svgcanvas/common/browser.js": {"lines":{"total":25,"covered":24,"skipped":0,"pct":96},"functions":{"total":6,"covered":2,"skipped":0,"pct":33.33},"statements":{"total":30,"covered":25,"skipped":0,"pct":83.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/packages/svgcanvas/common/util.js": {"lines":{"total":90,"covered":8,"skipped":0,"pct":8.89},"functions":{"total":7,"covered":3,"skipped":0,"pct":42.86},"statements":{"total":92,"covered":10,"skipped":0,"pct":10.87},"branches":{"total":98,"covered":10,"skipped":0,"pct":10.2}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/ConfigObj.js": {"lines":{"total":101,"covered":49,"skipped":0,"pct":48.51},"functions":{"total":14,"covered":11,"skipped":0,"pct":78.57},"statements":{"total":102,"covered":49,"skipped":0,"pct":48.04},"branches":{"total":96,"covered":33,"skipped":0,"pct":34.38}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/Editor.js": {"lines":{"total":410,"covered":184,"skipped":0,"pct":44.88},"functions":{"total":102,"covered":32,"skipped":0,"pct":31.37},"statements":{"total":417,"covered":185,"skipped":0,"pct":44.36},"branches":{"total":213,"covered":68,"skipped":0,"pct":31.92}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/EditorStartup.js": {"lines":{"total":346,"covered":211,"skipped":0,"pct":60.98},"functions":{"total":51,"covered":26,"skipped":0,"pct":50.98},"statements":{"total":357,"covered":219,"skipped":0,"pct":61.34},"branches":{"total":122,"covered":38,"skipped":0,"pct":31.15}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/MainMenu.js": {"lines":{"total":111,"covered":44,"skipped":0,"pct":39.64},"functions":{"total":14,"covered":7,"skipped":0,"pct":50},"statements":{"total":111,"covered":44,"skipped":0,"pct":39.64},"branches":{"total":48,"covered":7,"skipped":0,"pct":14.58}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/Rulers.js": {"lines":{"total":119,"covered":91,"skipped":0,"pct":76.47},"functions":{"total":6,"covered":4,"skipped":0,"pct":66.67},"statements":{"total":124,"covered":94,"skipped":0,"pct":75.81},"branches":{"total":43,"covered":30,"skipped":0,"pct":69.77}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/browser-not-supported.js": {"lines":{"total":4,"covered":3,"skipped":0,"pct":75},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":3,"skipped":0,"pct":75},"branches":{"total":4,"covered":3,"skipped":0,"pct":75}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/contextmenu.js": {"lines":{"total":22,"covered":9,"skipped":0,"pct":40.91},"functions":{"total":8,"covered":1,"skipped":0,"pct":12.5},"statements":{"total":23,"covered":9,"skipped":0,"pct":39.13},"branches":{"total":13,"covered":0,"skipped":0,"pct":0}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/locale.js": {"lines":{"total":14,"covered":9,"skipped":0,"pct":64.29},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":14,"covered":9,"skipped":0,"pct":64.29},"branches":{"total":10,"covered":3,"skipped":0,"pct":30}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/PaintBox.js": {"lines":{"total":64,"covered":35,"skipped":0,"pct":54.69},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":67,"covered":35,"skipped":0,"pct":52.24},"branches":{"total":33,"covered":12,"skipped":0,"pct":36.36}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/index.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seButton.js": {"lines":{"total":57,"covered":40,"skipped":0,"pct":70.18},"functions":{"total":15,"covered":7,"skipped":0,"pct":46.67},"statements":{"total":60,"covered":42,"skipped":0,"pct":70},"branches":{"total":33,"covered":23,"skipped":0,"pct":69.7}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seColorPicker.js": {"lines":{"total":50,"covered":41,"skipped":0,"pct":82},"functions":{"total":16,"covered":11,"skipped":0,"pct":68.75},"statements":{"total":51,"covered":41,"skipped":0,"pct":80.39},"branches":{"total":11,"covered":8,"skipped":0,"pct":72.73}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seExplorerButton.js": {"lines":{"total":95,"covered":72,"skipped":0,"pct":75.79},"functions":{"total":17,"covered":10,"skipped":0,"pct":58.82},"statements":{"total":98,"covered":72,"skipped":0,"pct":73.47},"branches":{"total":30,"covered":17,"skipped":0,"pct":56.67}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seFlyingButton.js": {"lines":{"total":74,"covered":47,"skipped":0,"pct":63.51},"functions":{"total":14,"covered":9,"skipped":0,"pct":64.29},"statements":{"total":75,"covered":48,"skipped":0,"pct":64},"branches":{"total":29,"covered":12,"skipped":0,"pct":41.38}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seInput.js": {"lines":{"total":46,"covered":31,"skipped":0,"pct":67.39},"functions":{"total":16,"covered":8,"skipped":0,"pct":50},"statements":{"total":47,"covered":31,"skipped":0,"pct":65.96},"branches":{"total":8,"covered":4,"skipped":0,"pct":50}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seList.js": {"lines":{"total":87,"covered":59,"skipped":0,"pct":67.82},"functions":{"total":21,"covered":10,"skipped":0,"pct":47.62},"statements":{"total":89,"covered":61,"skipped":0,"pct":68.54},"branches":{"total":22,"covered":12,"skipped":0,"pct":54.55}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seListItem.js": {"lines":{"total":39,"covered":26,"skipped":0,"pct":66.67},"functions":{"total":12,"covered":4,"skipped":0,"pct":33.33},"statements":{"total":40,"covered":27,"skipped":0,"pct":67.5},"branches":{"total":10,"covered":8,"skipped":0,"pct":80}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seMenu.js": {"lines":{"total":26,"covered":20,"skipped":0,"pct":76.92},"functions":{"total":7,"covered":3,"skipped":0,"pct":42.86},"statements":{"total":27,"covered":20,"skipped":0,"pct":74.07},"branches":{"total":5,"covered":3,"skipped":0,"pct":60}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seMenuItem.js": {"lines":{"total":37,"covered":26,"skipped":0,"pct":70.27},"functions":{"total":9,"covered":5,"skipped":0,"pct":55.56},"statements":{"total":40,"covered":27,"skipped":0,"pct":67.5},"branches":{"total":19,"covered":8,"skipped":0,"pct":42.11}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/sePalette.js": {"lines":{"total":33,"covered":32,"skipped":0,"pct":96.97},"functions":{"total":7,"covered":7,"skipped":0,"pct":100},"statements":{"total":33,"covered":32,"skipped":0,"pct":96.97},"branches":{"total":13,"covered":10,"skipped":0,"pct":76.92}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/sePlainBorderButton.js": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/sePlainMenuButton.js": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seSelect.js": {"lines":{"total":56,"covered":45,"skipped":0,"pct":80.36},"functions":{"total":17,"covered":9,"skipped":0,"pct":52.94},"statements":{"total":59,"covered":47,"skipped":0,"pct":79.66},"branches":{"total":16,"covered":14,"skipped":0,"pct":87.5}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seSpinInput.js": {"lines":{"total":65,"covered":55,"skipped":0,"pct":84.62},"functions":{"total":18,"covered":10,"skipped":0,"pct":55.56},"statements":{"total":66,"covered":55,"skipped":0,"pct":83.33},"branches":{"total":17,"covered":13,"skipped":0,"pct":76.47}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seText.js": {"lines":{"total":28,"covered":18,"skipped":0,"pct":64.29},"functions":{"total":10,"covered":4,"skipped":0,"pct":40},"statements":{"total":29,"covered":18,"skipped":0,"pct":62.07},"branches":{"total":8,"covered":5,"skipped":0,"pct":62.5}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/seZoom.js": {"lines":{"total":102,"covered":65,"skipped":0,"pct":63.73},"functions":{"total":28,"covered":9,"skipped":0,"pct":32.14},"statements":{"total":107,"covered":66,"skipped":0,"pct":61.68},"branches":{"total":36,"covered":7,"skipped":0,"pct":19.44}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/jgraduate/ColorValuePicker.js": {"lines":{"total":231,"covered":54,"skipped":0,"pct":23.38},"functions":{"total":9,"covered":3,"skipped":0,"pct":33.33},"statements":{"total":249,"covered":63,"skipped":0,"pct":25.3},"branches":{"total":157,"covered":34,"skipped":0,"pct":21.66}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/jgraduate/Slider.js": {"lines":{"total":156,"covered":91,"skipped":0,"pct":58.33},"functions":{"total":17,"covered":9,"skipped":0,"pct":52.94},"statements":{"total":176,"covered":96,"skipped":0,"pct":54.55},"branches":{"total":186,"covered":97,"skipped":0,"pct":52.15}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/jgraduate/jQuery.jGraduate.js": {"lines":{"total":580,"covered":275,"skipped":0,"pct":47.41},"functions":{"total":44,"covered":14,"skipped":0,"pct":31.82},"statements":{"total":602,"covered":282,"skipped":0,"pct":46.84},"branches":{"total":278,"covered":100,"skipped":0,"pct":35.97}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/components/jgraduate/jQuery.jPicker.js": {"lines":{"total":840,"covered":449,"skipped":0,"pct":53.45},"functions":{"total":60,"covered":39,"skipped":0,"pct":65},"statements":{"total":926,"covered":475,"skipped":0,"pct":51.3},"branches":{"total":777,"covered":327,"skipped":0,"pct":42.08}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/SePlainAlertDialog.js": {"lines":{"total":7,"covered":1,"skipped":0,"pct":14.29},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":7,"covered":1,"skipped":0,"pct":14.29},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/cmenuDialog.js": {"lines":{"total":120,"covered":112,"skipped":0,"pct":93.33},"functions":{"total":28,"covered":15,"skipped":0,"pct":53.57},"statements":{"total":131,"covered":116,"skipped":0,"pct":88.55},"branches":{"total":23,"covered":19,"skipped":0,"pct":82.61}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/cmenuLayersDialog.js": {"lines":{"total":61,"covered":49,"skipped":0,"pct":80.33},"functions":{"total":16,"covered":6,"skipped":0,"pct":37.5},"statements":{"total":66,"covered":49,"skipped":0,"pct":74.24},"branches":{"total":18,"covered":13,"skipped":0,"pct":72.22}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/editorPreferencesDialog.js": {"lines":{"total":157,"covered":124,"skipped":0,"pct":78.98},"functions":{"total":30,"covered":9,"skipped":0,"pct":30},"statements":{"total":159,"covered":125,"skipped":0,"pct":78.62},"branches":{"total":46,"covered":34,"skipped":0,"pct":73.91}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/exportDialog.js": {"lines":{"total":55,"covered":39,"skipped":0,"pct":70.91},"functions":{"total":14,"covered":5,"skipped":0,"pct":35.71},"statements":{"total":58,"covered":39,"skipped":0,"pct":67.24},"branches":{"total":12,"covered":6,"skipped":0,"pct":50}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/imagePropertiesDialog.js": {"lines":{"total":161,"covered":90,"skipped":0,"pct":55.9},"functions":{"total":20,"covered":5,"skipped":0,"pct":25},"statements":{"total":162,"covered":90,"skipped":0,"pct":55.56},"branches":{"total":53,"covered":23,"skipped":0,"pct":43.4}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/index.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/seAlertDialog.js": {"lines":{"total":6,"covered":2,"skipped":0,"pct":33.33},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":6,"covered":2,"skipped":0,"pct":33.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/seConfirmDialog.js": {"lines":{"total":8,"covered":2,"skipped":0,"pct":25},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":2,"skipped":0,"pct":25},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/sePromptDialog.js": {"lines":{"total":24,"covered":5,"skipped":0,"pct":20.83},"functions":{"total":7,"covered":2,"skipped":0,"pct":28.57},"statements":{"total":24,"covered":5,"skipped":0,"pct":20.83},"branches":{"total":9,"covered":0,"skipped":0,"pct":0}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/seSelectDialog.js": {"lines":{"total":8,"covered":2,"skipped":0,"pct":25},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":2,"skipped":0,"pct":25},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/svgSourceDialog.js": {"lines":{"total":74,"covered":56,"skipped":0,"pct":75.68},"functions":{"total":17,"covered":6,"skipped":0,"pct":35.29},"statements":{"total":75,"covered":56,"skipped":0,"pct":74.67},"branches":{"total":17,"covered":13,"skipped":0,"pct":76.47}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/se-elix/define/NumberSpinBox.js": {"lines":{"total":1,"covered":1,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":1,"covered":1,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js": {"lines":{"total":54,"covered":52,"skipped":0,"pct":96.3},"functions":{"total":13,"covered":11,"skipped":0,"pct":84.62},"statements":{"total":54,"covered":52,"skipped":0,"pct":96.3},"branches":{"total":53,"covered":47,"skipped":0,"pct":88.68}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-connector/ext-connector.js": {"lines":{"total":287,"covered":83,"skipped":0,"pct":28.92},"functions":{"total":26,"covered":16,"skipped":0,"pct":61.54},"statements":{"total":295,"covered":86,"skipped":0,"pct":29.15},"branches":{"total":140,"covered":19,"skipped":0,"pct":13.57}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-connector/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-eyedropper/ext-eyedropper.js": {"lines":{"total":60,"covered":39,"skipped":0,"pct":65},"functions":{"total":8,"covered":5,"skipped":0,"pct":62.5},"statements":{"total":71,"covered":39,"skipped":0,"pct":54.93},"branches":{"total":39,"covered":14,"skipped":0,"pct":35.9}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-eyedropper/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-grid/ext-grid.js": {"lines":{"total":88,"covered":43,"skipped":0,"pct":48.86},"functions":{"total":8,"covered":4,"skipped":0,"pct":50},"statements":{"total":90,"covered":43,"skipped":0,"pct":47.78},"branches":{"total":10,"covered":4,"skipped":0,"pct":40}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-grid/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-markers/ext-markers.js": {"lines":{"total":149,"covered":46,"skipped":0,"pct":30.87},"functions":{"total":21,"covered":12,"skipped":0,"pct":57.14},"statements":{"total":164,"covered":48,"skipped":0,"pct":29.27},"branches":{"total":80,"covered":22,"skipped":0,"pct":27.5}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-opensave/ext-opensave.js": {"lines":{"total":136,"covered":36,"skipped":0,"pct":26.47},"functions":{"total":13,"covered":3,"skipped":0,"pct":23.08},"statements":{"total":141,"covered":36,"skipped":0,"pct":25.53},"branches":{"total":32,"covered":0,"skipped":0,"pct":0}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-opensave/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-panning/ext-panning.js": {"lines":{"total":30,"covered":22,"skipped":0,"pct":73.33},"functions":{"total":7,"covered":6,"skipped":0,"pct":85.71},"statements":{"total":30,"covered":22,"skipped":0,"pct":73.33},"branches":{"total":6,"covered":2,"skipped":0,"pct":33.33}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-panning/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-polystar/ext-polystar.js": {"lines":{"total":247,"covered":232,"skipped":0,"pct":93.93},"functions":{"total":18,"covered":16,"skipped":0,"pct":88.89},"statements":{"total":256,"covered":241,"skipped":0,"pct":94.14},"branches":{"total":62,"covered":39,"skipped":0,"pct":62.9}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-polystar/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-shapes/ext-shapes.js": {"lines":{"total":76,"covered":74,"skipped":0,"pct":97.37},"functions":{"total":7,"covered":7,"skipped":0,"pct":100},"statements":{"total":80,"covered":78,"skipped":0,"pct":97.5},"branches":{"total":26,"covered":17,"skipped":0,"pct":65.38}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-shapes/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-storage/ext-storage.js": {"lines":{"total":91,"covered":59,"skipped":0,"pct":64.84},"functions":{"total":12,"covered":7,"skipped":0,"pct":58.33},"statements":{"total":91,"covered":59,"skipped":0,"pct":64.84},"branches":{"total":68,"covered":26,"skipped":0,"pct":38.24}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/extensions/ext-storage/storageDialog.js": {"lines":{"total":60,"covered":56,"skipped":0,"pct":93.33},"functions":{"total":10,"covered":7,"skipped":0,"pct":70},"statements":{"total":62,"covered":57,"skipped":0,"pct":91.94},"branches":{"total":15,"covered":13,"skipped":0,"pct":86.67}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/locale/lang.en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/panels/BottomPanel.js": {"lines":{"total":71,"covered":56,"skipped":0,"pct":78.87},"functions":{"total":22,"covered":14,"skipped":0,"pct":63.64},"statements":{"total":77,"covered":59,"skipped":0,"pct":76.62},"branches":{"total":30,"covered":12,"skipped":0,"pct":40}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/panels/LayersPanel.js": {"lines":{"total":157,"covered":70,"skipped":0,"pct":44.59},"functions":{"total":27,"covered":6,"skipped":0,"pct":22.22},"statements":{"total":163,"covered":71,"skipped":0,"pct":43.56},"branches":{"total":46,"covered":6,"skipped":0,"pct":13.04}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/panels/LeftPanel.js": {"lines":{"total":58,"covered":47,"skipped":0,"pct":81.03},"functions":{"total":20,"covered":15,"skipped":0,"pct":75},"statements":{"total":59,"covered":47,"skipped":0,"pct":79.66},"branches":{"total":32,"covered":12,"skipped":0,"pct":37.5}}
+,"/Users/jean-francoishenon/Work/svgedit/src/editor/panels/TopPanel.js": {"lines":{"total":405,"covered":282,"skipped":0,"pct":69.63},"functions":{"total":74,"covered":47,"skipped":0,"pct":63.51},"statements":{"total":416,"covered":285,"skipped":0,"pct":68.51},"branches":{"total":205,"covered":111,"skipped":0,"pct":54.15}}
}
diff --git a/cypress/__svgSnapshots__/scenario6-check tool_polygon_align_to_page.svg b/cypress/__svgSnapshots__/scenario6-check tool_polygon_align_to_page.svg
index 363dc28dc..9ad2cee9b 100644
--- a/cypress/__svgSnapshots__/scenario6-check tool_polygon_align_to_page.svg
+++ b/cypress/__svgSnapshots__/scenario6-check tool_polygon_align_to_page.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_sides.svg b/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_sides.svg
index a6d115807..d2f2a6c82 100644
--- a/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_sides.svg
+++ b/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_sides.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stoke_fill_color.svg b/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stoke_fill_color.svg
index 26d2d66e7..2301417e7 100644
--- a/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stoke_fill_color.svg
+++ b/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stoke_fill_color.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stroke_width.svg b/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stroke_width.svg
index d630c8a61..ecd4dd638 100644
--- a/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stroke_width.svg
+++ b/cypress/__svgSnapshots__/scenario6-check tool_polygon_change_stroke_width.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario7-check tool_star_align_to_page.svg b/cypress/__svgSnapshots__/scenario7-check tool_star_align_to_page.svg
index 489e3f5ff..e43174625 100644
--- a/cypress/__svgSnapshots__/scenario7-check tool_star_align_to_page.svg
+++ b/cypress/__svgSnapshots__/scenario7-check tool_star_align_to_page.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario7-check tool_star_change_sides.svg b/cypress/__svgSnapshots__/scenario7-check tool_star_change_sides.svg
index 7dd9fc9fd..cb997fc8e 100644
--- a/cypress/__svgSnapshots__/scenario7-check tool_star_change_sides.svg
+++ b/cypress/__svgSnapshots__/scenario7-check tool_star_change_sides.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario7-check tool_star_change_stoke_fill_color.svg b/cypress/__svgSnapshots__/scenario7-check tool_star_change_stoke_fill_color.svg
index 6da95a839..cc488d112 100644
--- a/cypress/__svgSnapshots__/scenario7-check tool_star_change_stoke_fill_color.svg
+++ b/cypress/__svgSnapshots__/scenario7-check tool_star_change_stoke_fill_color.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/__svgSnapshots__/scenario7-check tool_star_change_stroke_width.svg b/cypress/__svgSnapshots__/scenario7-check tool_star_change_stroke_width.svg
index 6a4d21ec7..774fc2cc7 100644
--- a/cypress/__svgSnapshots__/scenario7-check tool_star_change_stroke_width.svg
+++ b/cypress/__svgSnapshots__/scenario7-check tool_star_change_stroke_width.svg
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/cypress/e2e/unit/utilities-bbox.cy.js b/cypress/e2e/unit/utilities-bbox.cy.js
index 9e5913d87..0e181c857 100644
--- a/cypress/e2e/unit/utilities-bbox.cy.js
+++ b/cypress/e2e/unit/utilities-bbox.cy.js
@@ -5,6 +5,7 @@ import * as utilities from '../../../packages/svgcanvas/core/utilities.js'
import * as math from '../../../packages/svgcanvas/core/math.js'
import * as path from '../../../packages/svgcanvas/core/path.js'
import setAssertionMethods from '../../support/assert-close.js'
+import * as units from '../../../packages/svgcanvas/core/units.js'
// eslint-disable-next-line
chai.use(setAssertionMethods)
@@ -94,6 +95,7 @@ describe('utilities bbox', function () {
}
path.init(mockSvgCanvas)
+ units.init({ getRoundDigits: () => 2 }) // mock getRoundDigits
mockaddSVGElementsFromJsonCallCount = 0
})
diff --git a/cypress/e2e/unit/utilities-performance.cy.js b/cypress/e2e/unit/utilities-performance.cy.js
index 72a3c10f4..66e721810 100644
--- a/cypress/e2e/unit/utilities-performance.cy.js
+++ b/cypress/e2e/unit/utilities-performance.cy.js
@@ -4,9 +4,12 @@ import 'pathseg'
import { NS } from '../../../packages/svgcanvas/core/namespaces.js'
import * as utilities from '../../../packages/svgcanvas/core/utilities.js'
import * as math from '../../../packages/svgcanvas/core/math.js'
+import * as units from '../../../packages/svgcanvas/core/units.js'
+
describe('utilities performance', function () {
let currentLayer; let groupWithMatrixTransform; let textWithMatrixTransform
+ units.init({ getRoundDigits: () => 2 }) // mock getRoundDigits
beforeEach(() => {
document.body.textContent = ''
const style = document.createElement('style')
diff --git a/packages/svgcanvas/core/undo.js b/packages/svgcanvas/core/undo.js
index f822b27c2..fe8206bc6 100644
--- a/packages/svgcanvas/core/undo.js
+++ b/packages/svgcanvas/core/undo.js
@@ -169,7 +169,6 @@ export const changeSelectedAttributeNoUndoMethod = (attr, newValue, elems) => {
const bbox = getStrokedBBoxDefaultVisible([elem])
const diffX = attr === 'x' ? parseFloat(newValue) - bbox.x : 0
const diffY = attr === 'y' ? parseFloat(newValue) - bbox.y : 0
- console.log('diffX, diffY', diffX, diffY)
svgCanvas.moveSelectedElements(diffX * zoom, diffY * zoom, true)
continue
}
@@ -190,7 +189,7 @@ export const changeSelectedAttributeNoUndoMethod = (attr, newValue, elems) => {
} else if (attr === '#href') {
setHref(elem, newValue)
} else if (newValue) {
- elem.setAttribute(attr, parseFloat(newValue))
+ elem.setAttribute(attr, isNaN(parseFloat(newValue)) ? newValue : parseFloat(newValue))
} else if (typeof newValue === 'number') {
elem.setAttribute(attr, newValue)
} else {
diff --git a/packages/svgcanvas/core/utilities.js b/packages/svgcanvas/core/utilities.js
index fa953f8da..9be88f32c 100644
--- a/packages/svgcanvas/core/utilities.js
+++ b/packages/svgcanvas/core/utilities.js
@@ -7,14 +7,18 @@
*/
import { NS } from './namespaces.js'
-import { setUnitAttr, getTypeMap } from './units.js'
+import { setUnitAttr, getTypeMap, shortFloat } from './units.js'
import {
- hasMatrixTransform, transformListToTransform, transformBox, getTransformList
+ hasMatrixTransform,
+ transformListToTransform,
+ transformBox,
+ getTransformList
} from './math.js'
import { getClosest, mergeDeep } from '../common/util.js'
// Much faster than running getBBox() every time
-const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use,clipPath'
+const visElems =
+ 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use,clipPath'
const visElemsArr = visElems.split(',')
// const hidElems = 'defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';
@@ -22,15 +26,15 @@ let svgCanvas = null
let svgroot_ = null
/**
-* Object with the following keys/values.
-* @typedef {PlainObject} module:utilities.SVGElementJSON
-* @property {string} element - Tag name of the SVG element to create
-* @property {PlainObject} attr - Has key-value attributes to assign to the new element.
-* An `id` should be set so that {@link module:utilities.EditorContext#addSVGElementsFromJson} can later re-identify the element for modification or replacement.
-* @property {boolean} [curStyles=false] - Indicates whether current style attributes should be applied first
-* @property {module:utilities.SVGElementJSON[]} [children] - Data objects to be added recursively as children
-* @property {string} [namespace="http://www.w3.org/2000/svg"] - Indicate a (non-SVG) namespace
-*/
+ * Object with the following keys/values.
+ * @typedef {PlainObject} module:utilities.SVGElementJSON
+ * @property {string} element - Tag name of the SVG element to create
+ * @property {PlainObject} attr - Has key-value attributes to assign to the new element.
+ * An `id` should be set so that {@link module:utilities.EditorContext#addSVGElementsFromJson} can later re-identify the element for modification or replacement.
+ * @property {boolean} [curStyles=false] - Indicates whether current style attributes should be applied first
+ * @property {module:utilities.SVGElementJSON[]} [children] - Data objects to be added recursively as children
+ * @property {string} [namespace="http://www.w3.org/2000/svg"] - Indicate a (non-SVG) namespace
+ */
/**
* An object that creates SVG elements for the canvas.
@@ -49,38 +53,38 @@ let svgroot_ = null
* @function module:utilities.EditorContext#addSVGElementsFromJson
* @param {module:utilities.SVGElementJSON} data
* @returns {Element} The new element
-*/
+ */
/**
* @function module:utilities.EditorContext#getSelectedElements
* @returns {Element[]} the array with selected DOM elements
-*/
+ */
/**
* @function module:utilities.EditorContext#getDOMDocument
* @returns {HTMLDocument}
-*/
+ */
/**
* @function module:utilities.EditorContext#getDOMContainer
* @returns {HTMLElement}
-*/
+ */
/**
* @function module:utilities.EditorContext#getSvgRoot
* @returns {SVGSVGElement}
-*/
+ */
/**
* @function module:utilities.EditorContext#getBaseUnit
* @returns {string}
-*/
+ */
/**
* @function module:utilities.EditorContext#getSnappingStep
* @returns {Float|string}
-*/
+ */
/**
-* @function module:utilities.init
-* @param {module:utilities.EditorContext} canvas
-* @returns {void}
-*/
-export const init = (canvas) => {
+ * @function module:utilities.init
+ * @param {module:utilities.EditorContext} canvas
+ * @returns {void}
+ */
+export const init = canvas => {
svgCanvas = canvas
svgroot_ = canvas.getSvgRoot()
}
@@ -92,19 +96,19 @@ export const init = (canvas) => {
* @returns {string} The string with entity declarations in the internal subset removed
* @todo This might be needed in other places `parseFromString` is used even without LGTM flagging
*/
-export const dropXMLInternalSubset = (str) => {
+export const dropXMLInternalSubset = str => {
return str.replace(/()/, '$1$2')
// return str.replace(/(?\?\]>)/, '$$');
}
/**
-* Converts characters in a string to XML-friendly entities.
-* @function module:utilities.toXml
-* @example `&` becomes `&`
-* @param {string} str - The string to be converted
-* @returns {string} The converted string
-*/
-export const toXml = (str) => {
+ * Converts characters in a string to XML-friendly entities.
+ * @function module:utilities.toXml
+ * @example `&` becomes `&`
+ * @param {string} str - The string to be converted
+ * @returns {string} The converted string
+ */
+export const toXml = str => {
// ' is ok in XML, but not HTML
// > does not normally need escaping, though it can if within a CDATA expression (and preceded by "]]")
return str
@@ -123,11 +127,11 @@ export const toXml = (str) => {
// also precalculate the size of the array needed.
/**
-* Converts a string to base64.
-* @function module:utilities.encode64
-* @param {string} input
-* @returns {string} Base64 output
-*/
+ * Converts a string to base64.
+ * @function module:utilities.encode64
+ * @param {string} input
+ * @returns {string} Base64 output
+ */
export function encode64 (input) {
// base64 strings are 4/3 larger than the original string
input = encodeUTF8(input) // convert non-ASCII characters
@@ -135,11 +139,11 @@ export function encode64 (input) {
}
/**
-* Converts a string from base64.
-* @function module:utilities.decode64
-* @param {string} input Base64-encoded input
-* @returns {string} Decoded output
-*/
+ * Converts a string from base64.
+ * @function module:utilities.decode64
+ * @param {string} input Base64-encoded input
+ * @returns {string} Decoded output
+ */
export function decode64 (input) {
return decodeUTF8(window.atob(input))
}
@@ -155,28 +159,28 @@ export function hashCode (word) {
if (word.length === 0) return hash
for (let i = 0; i < word.length; i++) {
chr = word.charCodeAt(i)
- hash = ((hash << 5) - hash) + chr
+ hash = (hash << 5) - hash + chr
hash |= 0 // Convert to 32bit integer
}
return hash
}
/**
-* @function module:utilities.decodeUTF8
-* @param {string} argString
-* @returns {string}
-*/
+ * @function module:utilities.decodeUTF8
+ * @param {string} argString
+ * @returns {string}
+ */
export function decodeUTF8 (argString) {
return decodeURIComponent(escape(argString))
}
// codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
/**
-* @function module:utilities.encodeUTF8
-* @param {string} argString
-* @returns {string}
-*/
-export const encodeUTF8 = (argString) => {
+ * @function module:utilities.encodeUTF8
+ * @param {string} argString
+ * @returns {string}
+ */
+export const encodeUTF8 = argString => {
return unescape(encodeURIComponent(argString))
}
@@ -186,8 +190,13 @@ export const encodeUTF8 = (argString) => {
* @param {string} dataurl
* @returns {string} object URL or empty string
*/
-export const dataURLToObjectURL = (dataurl) => {
- if (typeof Uint8Array === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) {
+export const dataURLToObjectURL = dataurl => {
+ if (
+ typeof Uint8Array === 'undefined' ||
+ typeof Blob === 'undefined' ||
+ typeof URL === 'undefined' ||
+ !URL.createObjectURL
+ ) {
return ''
}
const arr = dataurl.split(',')
@@ -213,7 +222,7 @@ export const dataURLToObjectURL = (dataurl) => {
* @param {Blob} blob A Blob object or File object
* @returns {string} object URL or empty string
*/
-export const createObjectURL = (blob) => {
+export const createObjectURL = blob => {
if (!blob || typeof URL === 'undefined' || !URL.createObjectURL) {
return ''
}
@@ -227,39 +236,43 @@ export const blankPageObjectURL = (() => {
if (typeof Blob === 'undefined') {
return ''
}
- const blob = new Blob(['SVG-edit '], { type: 'text/html' })
+ const blob = new Blob(
+ ['SVG-edit '],
+ { type: 'text/html' }
+ )
return createObjectURL(blob)
})()
/**
-* Converts a string to use XML references (for non-ASCII).
-* @function module:utilities.convertToXMLReferences
-* @param {string} input
-* @returns {string} Decimal numeric character references
-*/
-export const convertToXMLReferences = (input) => {
- let output = '';
- [...input].forEach((ch) => {
+ * Converts a string to use XML references (for non-ASCII).
+ * @function module:utilities.convertToXMLReferences
+ * @param {string} input
+ * @returns {string} Decimal numeric character references
+ */
+export const convertToXMLReferences = input => {
+ let output = ''
+ ;[...input].forEach(ch => {
const c = ch.charCodeAt()
- output += (c <= 127) ? ch : `${c};`
+ output += c <= 127 ? ch : `${c};`
})
return output
}
/**
-* Cross-browser compatible method of converting a string to an XML tree.
-* Found this function [here]{@link http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f}.
-* @function module:utilities.text2xml
-* @param {string} sXML
-* @throws {Error}
-* @returns {XMLDocument}
-*/
-export const text2xml = (sXML) => {
+ * Cross-browser compatible method of converting a string to an XML tree.
+ * Found this function [here]{@link http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f}.
+ * @function module:utilities.text2xml
+ * @param {string} sXML
+ * @throws {Error}
+ * @returns {XMLDocument}
+ */
+export const text2xml = sXML => {
if (sXML.includes(' {
}
try {
out = dXML.parseFromString(sXML, 'text/xml')
- } catch (e2) { throw new Error('Error parsing XML string') }
+ } catch (e2) {
+ throw new Error('Error parsing XML string')
+ }
return out
}
/**
-* @typedef {PlainObject} module:utilities.BBoxObject (like `DOMRect`)
-* @property {Float} x
-* @property {Float} y
-* @property {Float} width
-* @property {Float} height
-*/
+ * @typedef {PlainObject} module:utilities.BBoxObject (like `DOMRect`)
+ * @property {Float} x
+ * @property {Float} y
+ * @property {Float} width
+ * @property {Float} height
+ */
/**
-* Converts a `SVGRect` into an object.
-* @function module:utilities.bboxToObj
-* @param {SVGRect} bbox - a SVGRect
-* @returns {module:utilities.BBoxObject} An object with properties names x, y, width, height.
-*/
+ * Converts a `SVGRect` into an object.
+ * @function module:utilities.bboxToObj
+ * @param {SVGRect} bbox - a SVGRect
+ * @returns {module:utilities.BBoxObject} An object with properties names x, y, width, height.
+ */
export const bboxToObj = ({ x, y, width, height }) => {
return { x, y, width, height }
}
/**
-* @callback module:utilities.TreeWalker
-* @param {Element} elem - DOM element being traversed
-* @returns {void}
-*/
+ * @callback module:utilities.TreeWalker
+ * @param {Element} elem - DOM element being traversed
+ * @returns {void}
+ */
/**
-* Walks the tree and executes the callback on each element in a top-down fashion.
-* @function module:utilities.walkTree
-* @param {Element} elem - DOM element to traverse
-* @param {module:utilities.TreeWalker} cbFn - Callback function to run on each element
-* @returns {void}
-*/
+ * Walks the tree and executes the callback on each element in a top-down fashion.
+ * @function module:utilities.walkTree
+ * @param {Element} elem - DOM element to traverse
+ * @param {module:utilities.TreeWalker} cbFn - Callback function to run on each element
+ * @returns {void}
+ */
export const walkTree = (elem, cbFn) => {
if (elem?.nodeType === 1) {
cbFn(elem)
@@ -314,13 +329,13 @@ export const walkTree = (elem, cbFn) => {
}
/**
-* Walks the tree and executes the callback on each element in a depth-first fashion.
-* @function module:utilities.walkTreePost
-* @todo Shouldn't this be calling walkTreePost?
-* @param {Element} elem - DOM element to traverse
-* @param {module:utilities.TreeWalker} cbFn - Callback function to run on each element
-* @returns {void}
-*/
+ * Walks the tree and executes the callback on each element in a depth-first fashion.
+ * @function module:utilities.walkTreePost
+ * @todo Shouldn't this be calling walkTreePost?
+ * @param {Element} elem - DOM element to traverse
+ * @param {module:utilities.TreeWalker} cbFn - Callback function to run on each element
+ * @returns {void}
+ */
export const walkTreePost = (elem, cbFn) => {
if (elem?.nodeType === 1) {
let i = elem.childNodes.length
@@ -332,15 +347,15 @@ export const walkTreePost = (elem, cbFn) => {
}
/**
-* Extracts the URL from the `url(...)` syntax of some attributes.
-* Three variants:
-* - ``
-* - ``
-* - ``
-* @function module:utilities.getUrlFromAttr
-* @param {string} attrVal The attribute value as a string
-* @returns {string} String with just the URL, like "someFile.svg#foo"
-*/
+ * Extracts the URL from the `url(...)` syntax of some attributes.
+ * Three variants:
+ * - ``
+ * - ``
+ * - ``
+ * @function module:utilities.getUrlFromAttr
+ * @param {string} attrVal The attribute value as a string
+ * @returns {string} String with just the URL, like "someFile.svg#foo"
+ */
export const getUrlFromAttr = function (attrVal) {
if (attrVal) {
// url('#somegrad')
@@ -359,29 +374,29 @@ export const getUrlFromAttr = function (attrVal) {
}
/**
-* @function module:utilities.getHref
-* @param {Element} elem
-* @returns {string} The given element's `xlink:href` value
-*/
+ * @function module:utilities.getHref
+ * @param {Element} elem
+ * @returns {string} The given element's `xlink:href` value
+ */
export let getHref = function (elem) {
return elem.getAttributeNS(NS.XLINK, 'href')
}
/**
-* Sets the given element's `xlink:href` value.
-* @function module:utilities.setHref
-* @param {Element} elem
-* @param {string} val
-* @returns {void}
-*/
+ * Sets the given element's `xlink:href` value.
+ * @function module:utilities.setHref
+ * @param {Element} elem
+ * @param {string} val
+ * @returns {void}
+ */
export let setHref = function (elem, val) {
elem.setAttributeNS(NS.XLINK, 'xlink:href', val)
}
/**
-* @function module:utilities.findDefs
-* @returns {SVGDefsElement} The document's `` element, creating it first if necessary
-*/
+ * @function module:utilities.findDefs
+ * @returns {SVGDefsElement} The document's `` element, creating it first if necessary
+ */
export const findDefs = function () {
const svgElement = svgCanvas.getSvgContent()
let defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs')
@@ -403,12 +418,12 @@ export const findDefs = function () {
// TODO(codedread): Consider moving the next to functions to bbox.js
/**
-* Get correct BBox for a path in Webkit.
-* Converted from code found [here]{@link http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html}.
-* @function module:utilities.getPathBBox
-* @param {SVGPathElement} path - The path DOM element to get the BBox for
-* @returns {module:utilities.BBoxObject} A BBox-like object
-*/
+ * Get correct BBox for a path in Webkit.
+ * Converted from code found [here]{@link http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html}.
+ * @function module:utilities.getPathBBox
+ * @param {SVGPathElement} path - The path DOM element to get the BBox for
+ * @returns {module:utilities.BBoxObject} A BBox-like object
+ */
export const getPathBBox = function (path) {
const seglist = path.pathSegList
const tot = seglist.numberOfItems
@@ -419,17 +434,23 @@ export const getPathBBox = function (path) {
const getCalc = function (j, P1, P2, P3) {
return function (t) {
- return 1 - t ** 3 * P0[j] +
- 3 * 1 - t ** 2 * t * P1[j] +
+ return (
+ 1 -
+ t ** 3 * P0[j] +
+ 3 * 1 -
+ t ** 2 * t * P1[j] +
3 * (1 - t) * t ** 2 * P2[j] +
t ** 3 * P3[j]
+ )
}
}
for (let i = 0; i < tot; i++) {
const seg = seglist.getItem(i)
- if (seg.x === undefined) { continue }
+ if (seg.x === undefined) {
+ continue
+ }
// Add actual points to limits
bounds[0].push(P0[0])
@@ -448,7 +469,9 @@ export const getPathBBox = function (path) {
const c = 3 * P1[j] - 3 * P0[j]
if (a === 0) {
- if (b === 0) { continue }
+ if (b === 0) {
+ continue
+ }
const t = -c / b
if (t > 0 && t < 1) {
bounds[j].push(calc(t))
@@ -456,11 +479,17 @@ export const getPathBBox = function (path) {
continue
}
const b2ac = b ** 2 - 4 * c * a
- if (b2ac < 0) { continue }
+ if (b2ac < 0) {
+ continue
+ }
const t1 = (-b + Math.sqrt(b2ac)) / (2 * a)
- if (t1 > 0 && t1 < 1) { bounds[j].push(calc(t1)) }
+ if (t1 > 0 && t1 < 1) {
+ bounds[j].push(calc(t1))
+ }
const t2 = (-b - Math.sqrt(b2ac)) / (2 * a)
- if (t2 > 0 && t2 < 1) { bounds[j].push(calc(t2)) }
+ if (t2 > 0 && t2 < 1) {
+ bounds[j].push(calc(t2))
+ }
}
P0 = P3
} else {
@@ -482,15 +511,17 @@ export const getPathBBox = function (path) {
}
/**
-* Get the given/selected element's bounding box object, convert it to be more
-* usable when necessary.
-* @function module:utilities.getBBox
-* @param {Element} elem - Optional DOM element to get the BBox for
-* @returns {module:utilities.BBoxObject} Bounding box object
-*/
+ * Get the given/selected element's bounding box object, convert it to be more
+ * usable when necessary.
+ * @function module:utilities.getBBox
+ * @param {Element} elem - Optional DOM element to get the BBox for
+ * @returns {module:utilities.BBoxObject} Bounding box object
+ */
export const getBBox = function (elem) {
const selected = elem || svgCanvas.getSelectedElements()[0]
- if (elem.nodeType !== 1) { return null }
+ if (elem.nodeType !== 1) {
+ return null
+ }
const elname = selected.nodeName
let ret = null
@@ -512,7 +543,6 @@ export const getBBox = function (elem) {
}
break
default:
-
if (elname === 'use') {
ret = selected.getBBox() // , true);
} else if (visElemsArr.includes(elname)) {
@@ -520,8 +550,8 @@ export const getBBox = function (elem) {
try {
ret = selected.getBBox()
} catch (err) {
- // tspan (and textPath apparently) have no `getBBox` in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=937268
- // Re: Chrome returning bbox for containing text element, see: https://bugs.chromium.org/p/chromium/issues/detail?id=349835
+ // tspan (and textPath apparently) have no `getBBox` in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=937268
+ // Re: Chrome returning bbox for containing text element, see: https://bugs.chromium.org/p/chromium/issues/detail?id=349835
const extent = selected.getExtentOfChar(0) // pos+dimensions of the first glyph
const width = selected.getComputedTextLength() // width of the tspan
ret = {
@@ -532,7 +562,7 @@ export const getBBox = function (elem) {
}
}
} else {
- // Check if element is child of a foreignObject
+ // Check if element is child of a foreignObject
const fo = getClosest(selected.parentNode, 'foreignObject')
if (fo.length && fo[0].getBBox) {
ret = fo[0].getBBox()
@@ -549,26 +579,26 @@ export const getBBox = function (elem) {
}
/**
-* @typedef {GenericArray} module:utilities.PathSegmentArray
-* @property {Integer} length 2
-* @property {"M"|"L"|"C"|"Z"} 0
-* @property {Float[]} 1
-*/
+ * @typedef {GenericArray} module:utilities.PathSegmentArray
+ * @property {Integer} length 2
+ * @property {"M"|"L"|"C"|"Z"} 0
+ * @property {Float[]} 1
+ */
/**
-* Create a path 'd' attribute from path segments.
-* Each segment is an array of the form: `[singleChar, [x,y, x,y, ...]]`
-* @function module:utilities.getPathDFromSegments
-* @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
-* @returns {string} The converted path d attribute.
-*/
+ * Create a path 'd' attribute from path segments.
+ * Each segment is an array of the form: `[singleChar, [x,y, x,y, ...]]`
+ * @function module:utilities.getPathDFromSegments
+ * @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
+ * @returns {string} The converted path d attribute.
+ */
export const getPathDFromSegments = function (pathSegments) {
let d = ''
pathSegments.forEach(function ([singleChar, pts], _j) {
d += singleChar
for (let i = 0; i < pts.length; i += 2) {
- d += (pts[i] + ',' + pts[i + 1]) + ' '
+ d += pts[i] + ',' + pts[i + 1] + ' '
}
})
@@ -576,15 +606,17 @@ export const getPathDFromSegments = function (pathSegments) {
}
/**
-* Make a path 'd' attribute from a simple SVG element shape.
-* @function module:utilities.getPathDFromElement
-* @param {Element} elem - The element to be converted
-* @returns {string} The path d attribute or `undefined` if the element type is unknown.
-*/
+ * Make a path 'd' attribute from a simple SVG element shape.
+ * @function module:utilities.getPathDFromElement
+ * @param {Element} elem - The element to be converted
+ * @returns {string} The path d attribute or `undefined` if the element type is unknown.
+ */
export const getPathDFromElement = function (elem) {
// Possibly the cubed root of 6, but 1.81 works best
let num = 1.81
- let d; let rx; let ry
+ let d
+ let rx
+ let ry
switch (elem.tagName) {
case 'ellipse':
case 'circle': {
@@ -597,24 +629,26 @@ export const getPathDFromElement = function (elem) {
rx = ry
}
d = getPathDFromSegments([
- ['M', [(cx - rx), (cy)]],
- ['C', [(cx - rx), (cy - ry / num), (cx - rx / num), (cy - ry), (cx), (cy - ry)]],
- ['C', [(cx + rx / num), (cy - ry), (cx + rx), (cy - ry / num), (cx + rx), (cy)]],
- ['C', [(cx + rx), (cy + ry / num), (cx + rx / num), (cy + ry), (cx), (cy + ry)]],
- ['C', [(cx - rx / num), (cy + ry), (cx - rx), (cy + ry / num), (cx - rx), (cy)]],
+ ['M', [cx - rx, cy]],
+ ['C', [cx - rx, cy - ry / num, cx - rx / num, cy - ry, cx, cy - ry]],
+ ['C', [cx + rx / num, cy - ry, cx + rx, cy - ry / num, cx + rx, cy]],
+ ['C', [cx + rx, cy + ry / num, cx + rx / num, cy + ry, cx, cy + ry]],
+ ['C', [cx - rx / num, cy + ry, cx - rx, cy + ry / num, cx - rx, cy]],
['Z', []]
])
break
- } case 'path':
+ }
+ case 'path':
d = elem.getAttribute('d')
break
- case 'line': {
- const x1 = elem.getAttribute('x1')
- const y1 = elem.getAttribute('y1')
- const x2 = elem.getAttribute('x2')
- const y2 = elem.getAttribute('y2')
- d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
- }
+ case 'line':
+ {
+ const x1 = elem.getAttribute('x1')
+ const y1 = elem.getAttribute('y1')
+ const x2 = elem.getAttribute('x2')
+ const y2 = elem.getAttribute('y2')
+ d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
+ }
break
case 'polyline':
d = 'M' + elem.getAttribute('points')
@@ -647,14 +681,25 @@ export const getPathDFromElement = function (elem) {
['L', [x + w - rx, y]],
['C', [x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry]],
['L', [x + w, y + h - ry]],
- ['C', [x + w, y + h - ry / num, x + w - rx / num, y + h, x + w - rx, y + h]],
+ [
+ 'C',
+ [
+ x + w,
+ y + h - ry / num,
+ x + w - rx / num,
+ y + h,
+ x + w - rx,
+ y + h
+ ]
+ ],
['L', [x + rx, y + h]],
['C', [x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry]],
['L', [x, y + ry]],
['Z', []]
])
break
- } default:
+ }
+ default:
break
}
@@ -662,33 +707,39 @@ export const getPathDFromElement = function (elem) {
}
/**
-* Get a set of attributes from an element that is useful for convertToPath.
-* @function module:utilities.getExtraAttributesForConvertToPath
-* @param {Element} elem - The element to be probed
-* @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
-*/
+ * Get a set of attributes from an element that is useful for convertToPath.
+ * @function module:utilities.getExtraAttributesForConvertToPath
+ * @param {Element} elem - The element to be probed
+ * @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
+ */
export const getExtraAttributesForConvertToPath = function (elem) {
- const attrs = {};
+ const attrs = {}
// TODO: make this list global so that we can properly maintain it
// TODO: what about @transform, @clip-rule, @fill-rule, etc?
- ['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'].forEach(function (item) {
- const a = elem.getAttribute(item)
- if (a) {
- attrs[item] = a
+ ;['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'].forEach(
+ function (item) {
+ const a = elem.getAttribute(item)
+ if (a) {
+ attrs[item] = a
+ }
}
- })
+ )
return attrs
}
/**
-* Get the BBox of an element-as-path.
-* @function module:utilities.getBBoxOfElementAsPath
-* @param {Element} elem - The DOM element to be probed
-* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
-* @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
-* @returns {DOMRect|false} The resulting path's bounding box object.
-*/
-export const getBBoxOfElementAsPath = function (elem, addSVGElementsFromJson, pathActions) {
+ * Get the BBox of an element-as-path.
+ * @function module:utilities.getBBoxOfElementAsPath
+ * @param {Element} elem - The DOM element to be probed
+ * @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
+ * @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
+ * @returns {DOMRect|false} The resulting path's bounding box object.
+ */
+export const getBBoxOfElementAsPath = function (
+ elem,
+ addSVGElementsFromJson,
+ pathActions
+) {
const path = addSVGElementsFromJson({
element: 'path',
attr: getExtraAttributesForConvertToPath(elem)
@@ -726,18 +777,18 @@ export const getBBoxOfElementAsPath = function (elem, addSVGElementsFromJson, pa
}
/**
-* Convert selected element to a path.
-* @function module:utilities.convertToPath
-* @param {Element} elem - The DOM element to be converted
-* @param {module:utilities.SVGElementJSON} attrs - Apply attributes to new path. see canvas.convertToPath
-* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
-* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
-* @param {module:draw.DrawCanvasInit#clearSelection|module:path.EditorContext#clearSelection} clearSelection - see [canvas.clearSelection]{@link module:svgcanvas.SvgCanvas#clearSelection}
-* @param {module:path.EditorContext#addToSelection} addToSelection - see [canvas.addToSelection]{@link module:svgcanvas.SvgCanvas#addToSelection}
-* @param {module:history} hstry - see history module
-* @param {module:path.EditorContext#addCommandToHistory|module:draw.DrawCanvasInit#addCommandToHistory} addCommandToHistory - see [canvas.addCommandToHistory]{@link module:svgcanvas~addCommandToHistory}
-* @returns {SVGPathElement|null} The converted path element or null if the DOM element was not recognized.
-*/
+ * Convert selected element to a path.
+ * @function module:utilities.convertToPath
+ * @param {Element} elem - The DOM element to be converted
+ * @param {module:utilities.SVGElementJSON} attrs - Apply attributes to new path. see canvas.convertToPath
+ * @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
+ * @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
+ * @param {module:draw.DrawCanvasInit#clearSelection|module:path.EditorContext#clearSelection} clearSelection - see [canvas.clearSelection]{@link module:svgcanvas.SvgCanvas#clearSelection}
+ * @param {module:path.EditorContext#addToSelection} addToSelection - see [canvas.addToSelection]{@link module:svgcanvas.SvgCanvas#addToSelection}
+ * @param {module:history} hstry - see history module
+ * @param {module:path.EditorContext#addCommandToHistory|module:draw.DrawCanvasInit#addCommandToHistory} addCommandToHistory - see [canvas.addCommandToHistory]{@link module:svgcanvas~addCommandToHistory}
+ * @returns {SVGPathElement|null} The converted path element or null if the DOM element was not recognized.
+ */
export const convertToPath = (elem, attrs, svgCanvas) => {
const batchCmd = new svgCanvas.history.BatchCommand('Convert element to Path')
@@ -777,7 +828,13 @@ export const convertToPath = (elem, attrs, svgCanvas) => {
}
const { nextSibling } = elem
- batchCmd.addSubCommand(new svgCanvas.history.RemoveElementCommand(elem, nextSibling, elem.parentNode))
+ batchCmd.addSubCommand(
+ new svgCanvas.history.RemoveElementCommand(
+ elem,
+ nextSibling,
+ elem.parentNode
+ )
+ )
svgCanvas.clearSelection()
elem.remove() // We need to remove this element otherwise the nextSibling of 'path' won't be null and an exception will be thrown after subsequent undo and redos.
@@ -796,25 +853,25 @@ export const convertToPath = (elem, attrs, svgCanvas) => {
}
/**
-* Can the bbox be optimized over the native getBBox? The optimized bbox is the same as the native getBBox when
-* the rotation angle is a multiple of 90 degrees and there are no complex transforms.
-* Getting an optimized bbox can be dramatically slower, so we want to make sure it's worth it.
-*
-* The best example for this is a circle rotate 45 degrees. The circle doesn't get wider or taller when rotated
-* about it's center.
-*
-* The standard, unoptimized technique gets the native bbox of the circle, rotates the box 45 degrees, uses
-* that width and height, and applies any transforms to get the final bbox. This means the calculated bbox
-* is much wider than the original circle. If the angle had been 0, 90, 180, etc. both techniques render the
-* same bbox.
-*
-* The optimization is not needed if the rotation is a multiple 90 degrees. The default technique is to call
-* getBBox then apply the angle and any transforms.
-*
-* @param {Float} angle - The rotation angle in degrees
-* @param {boolean} hasAMatrixTransform - True if there is a matrix transform
-* @returns {boolean} True if the bbox can be optimized.
-*/
+ * Can the bbox be optimized over the native getBBox? The optimized bbox is the same as the native getBBox when
+ * the rotation angle is a multiple of 90 degrees and there are no complex transforms.
+ * Getting an optimized bbox can be dramatically slower, so we want to make sure it's worth it.
+ *
+ * The best example for this is a circle rotate 45 degrees. The circle doesn't get wider or taller when rotated
+ * about it's center.
+ *
+ * The standard, unoptimized technique gets the native bbox of the circle, rotates the box 45 degrees, uses
+ * that width and height, and applies any transforms to get the final bbox. This means the calculated bbox
+ * is much wider than the original circle. If the angle had been 0, 90, 180, etc. both techniques render the
+ * same bbox.
+ *
+ * The optimization is not needed if the rotation is a multiple 90 degrees. The default technique is to call
+ * getBBox then apply the angle and any transforms.
+ *
+ * @param {Float} angle - The rotation angle in degrees
+ * @param {boolean} hasAMatrixTransform - True if there is a matrix transform
+ * @returns {boolean} True if the bbox can be optimized.
+ */
function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
const angleModulo90 = angle % 90
const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99
@@ -823,14 +880,18 @@ function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
}
/**
-* Get bounding box that includes any transforms.
-* @function module:utilities.getBBoxWithTransform
-* @param {Element} elem - The DOM element to be converted
-* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
-* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
-* @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
-*/
-export const getBBoxWithTransform = function (elem, addSVGElementsFromJson, pathActions) {
+ * Get bounding box that includes any transforms.
+ * @function module:utilities.getBBoxWithTransform
+ * @param {Element} elem - The DOM element to be converted
+ * @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
+ * @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
+ * @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
+ */
+export const getBBoxWithTransform = function (
+ elem,
+ addSVGElementsFromJson,
+ pathActions
+) {
// TODO: Fix issue with rotated groups. Currently they work
// fine in FF, but not in other browsers (same problem mentioned
// in Issue 339 comment #2).
@@ -852,14 +913,22 @@ export const getBBoxWithTransform = function (elem, addSVGElementsFromJson, path
// TODO: why ellipse and not circle
const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
if (elemNames.includes(elem.tagName)) {
- goodBb = getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions)
+ goodBb = getBBoxOfElementAsPath(
+ elem,
+ addSVGElementsFromJson,
+ pathActions
+ )
bb = goodBb
} else if (elem.tagName === 'rect') {
// Look for radius
const rx = Number(elem.getAttribute('rx'))
const ry = Number(elem.getAttribute('ry'))
if (rx || ry) {
- goodBb = getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions)
+ goodBb = getBBoxOfElementAsPath(
+ elem,
+ addSVGElementsFromJson,
+ pathActions
+ )
bb = goodBb
}
}
@@ -879,9 +948,9 @@ export const getBBoxWithTransform = function (elem, addSVGElementsFromJson, path
* @todo This is problematic with large stroke-width and, for example, a single
* horizontal line. The calculated BBox extends way beyond left and right sides.
*/
-const getStrokeOffsetForBBox = (elem) => {
+const getStrokeOffsetForBBox = elem => {
const sw = elem.getAttribute('stroke-width')
- return (!isNaN(sw) && elem.getAttribute('stroke') !== 'none') ? sw / 2 : 0
+ return !isNaN(sw) && elem.getAttribute('stroke') !== 'none' ? sw / 2 : 0
}
/**
@@ -893,25 +962,33 @@ const getStrokeOffsetForBBox = (elem) => {
*/
/**
-* Get the bounding box for one or more stroked and/or transformed elements.
-* @function module:utilities.getStrokedBBox
-* @param {Element[]} elems - Array with DOM elements to check
-* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
-* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
-* @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
-*/
+ * Get the bounding box for one or more stroked and/or transformed elements.
+ * @function module:utilities.getStrokedBBox
+ * @param {Element[]} elems - Array with DOM elements to check
+ * @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
+ * @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
+ * @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
+ */
export const getStrokedBBox = (elems, addSVGElementsFromJson, pathActions) => {
- if (!elems || !elems.length) { return false }
+ if (!elems || !elems.length) {
+ return false
+ }
let fullBb
- elems.forEach((elem) => {
- if (fullBb) { return }
- if (!elem.parentNode) { return }
+ elems.forEach(elem => {
+ if (fullBb) {
+ return
+ }
+ if (!elem.parentNode) {
+ return
+ }
fullBb = getBBoxWithTransform(elem, addSVGElementsFromJson, pathActions)
})
// This shouldn't ever happen...
- if (!fullBb) { return null }
+ if (!fullBb) {
+ return null
+ }
// fullBb doesn't include the stoke, so this does no good!
// if (elems.length == 1) return fullBb;
@@ -929,8 +1006,12 @@ export const getStrokedBBox = (elems, addSVGElementsFromJson, pathActions) => {
maxX += offset
maxY += offset
} else {
- elems.forEach((elem) => {
- const curBb = getBBoxWithTransform(elem, addSVGElementsFromJson, pathActions)
+ elems.forEach(elem => {
+ const curBb = getBBoxWithTransform(
+ elem,
+ addSVGElementsFromJson,
+ pathActions
+ )
if (curBb) {
const offset = getStrokeOffsetForBBox(elem)
minX = Math.min(minX, curBb.x - offset)
@@ -944,28 +1025,33 @@ export const getStrokedBBox = (elems, addSVGElementsFromJson, pathActions) => {
})
}
- fullBb.x = minX.toFixed(2)
- fullBb.y = minY.toFixed(2)
- fullBb.width = (maxX - minX).toFixed(2)
- fullBb.height = (maxY - minY).toFixed(2)
+ fullBb.x = shortFloat(minX)
+ fullBb.y = shortFloat(minY)
+ fullBb.width = shortFloat(maxX - minX)
+ fullBb.height = shortFloat(maxY - minY)
return fullBb
}
/**
-* Get all elements that have a BBox (excludes ``, ``, etc).
-* Note that 0-opacity, off-screen etc elements are still considered "visible"
-* for this function.
-* @function module:utilities.getVisibleElements
-* @param {Element} parentElement - The parent DOM element to search within
-* @returns {Element[]} All "visible" elements.
-*/
-export const getVisibleElements = (parentElement) => {
+ * Get all elements that have a BBox (excludes ``, ``, etc).
+ * Note that 0-opacity, off-screen etc elements are still considered "visible"
+ * for this function.
+ * @function module:utilities.getVisibleElements
+ * @param {Element} parentElement - The parent DOM element to search within
+ * @returns {Element[]} All "visible" elements.
+ */
+export const getVisibleElements = parentElement => {
if (!parentElement) {
const svgContent = svgCanvas.getSvgContent()
for (let i = 0; i < svgContent.children.length; i++) {
if (svgContent.children[i].getBBox) {
const bbox = svgContent.children[i].getBBox()
- if (bbox.width !== 0 && bbox.height !== 0 && bbox.width !== 0 && bbox.height !== 0) {
+ if (
+ bbox.width !== 0 &&
+ bbox.height !== 0 &&
+ bbox.width !== 0 &&
+ bbox.height !== 0
+ ) {
parentElement = svgContent.children[i]
break
}
@@ -977,7 +1063,7 @@ export const getVisibleElements = (parentElement) => {
if (parentElement) {
const children = parentElement.children
// eslint-disable-next-line array-callback-return
- Array.from(children, (elem) => {
+ Array.from(children, elem => {
if (elem.getBBox) {
contentElems.push(elem)
}
@@ -987,13 +1073,15 @@ export const getVisibleElements = (parentElement) => {
}
/**
-* Get the bounding box for one or more stroked and/or transformed elements.
-* @function module:utilities.getStrokedBBoxDefaultVisible
-* @param {Element[]} elems - Array with DOM elements to check
-* @returns {module:utilities.BBoxObject} A single bounding box object
-*/
-export const getStrokedBBoxDefaultVisible = (elems) => {
- if (!elems) { elems = getVisibleElements() }
+ * Get the bounding box for one or more stroked and/or transformed elements.
+ * @function module:utilities.getStrokedBBoxDefaultVisible
+ * @param {Element[]} elems - Array with DOM elements to check
+ * @returns {module:utilities.BBoxObject} A single bounding box object
+ */
+export const getStrokedBBoxDefaultVisible = elems => {
+ if (!elems) {
+ elems = getVisibleElements()
+ }
return getStrokedBBox(
elems,
svgCanvas.addSVGElementsFromJson,
@@ -1002,30 +1090,32 @@ export const getStrokedBBoxDefaultVisible = (elems) => {
}
/**
-* Get the rotation angle of the given transform list.
-* @function module:utilities.getRotationAngleFromTransformList
-* @param {SVGTransformList} tlist - List of transforms
-* @param {boolean} toRad - When true returns the value in radians rather than degrees
-* @returns {Float} The angle in degrees or radians
-*/
+ * Get the rotation angle of the given transform list.
+ * @function module:utilities.getRotationAngleFromTransformList
+ * @param {SVGTransformList} tlist - List of transforms
+ * @param {boolean} toRad - When true returns the value in radians rather than degrees
+ * @returns {Float} The angle in degrees or radians
+ */
export const getRotationAngleFromTransformList = (tlist, toRad) => {
- if (!tlist) { return 0 } //