diff --git a/src/editor/images/align_distrib_horiz.svg b/src/editor/images/align_distrib_horiz.svg
new file mode 100644
index 00000000..394f1562
--- /dev/null
+++ b/src/editor/images/align_distrib_horiz.svg
@@ -0,0 +1 @@
+
diff --git a/src/editor/images/align_distrib_verti.svg b/src/editor/images/align_distrib_verti.svg
new file mode 100644
index 00000000..99053642
--- /dev/null
+++ b/src/editor/images/align_distrib_verti.svg
@@ -0,0 +1 @@
+
diff --git a/src/editor/panels/TopPanel.html b/src/editor/panels/TopPanel.html
index c70eed64..d3f60af4 100644
--- a/src/editor/panels/TopPanel.html
+++ b/src/editor/panels/TopPanel.html
@@ -52,6 +52,10 @@
img-height="22px">
+
+
@@ -79,6 +83,8 @@
+
+
diff --git a/src/editor/panels/TopPanel.js b/src/editor/panels/TopPanel.js
index 5d0f03f3..4551fb0c 100644
--- a/src/editor/panels/TopPanel.js
+++ b/src/editor/panels/TopPanel.js
@@ -944,6 +944,8 @@ class TopPanel {
$click($id('tool_align_top'), () => this.clickAlign.bind(this)('top'))
$click($id('tool_align_bottom'), () => this.clickAlign.bind(this)('bottom'))
$click($id('tool_align_middle'), () => this.clickAlign.bind(this)('middle'))
+ $click($id('tool_align_distrib_horiz'), () => this.clickAlign.bind(this)('distrib_horiz'))
+ $click($id('tool_align_distrib_verti'), () => this.clickAlign.bind(this)('distrib_verti'))
$click($id('tool_node_clone'), this.clonePathNode.bind(this))
$click($id('tool_node_delete'), this.deletePathNode.bind(this))
$click($id('tool_openclose_path'), this.opencloseSubPath.bind(this))
diff --git a/src/svgcanvas/selected-elem.js b/src/svgcanvas/selected-elem.js
index 8943e5fc..9c66aa3c 100644
--- a/src/svgcanvas/selected-elem.js
+++ b/src/svgcanvas/selected-elem.js
@@ -323,94 +323,222 @@ const alignSelectedElements = (type, relativeTo) => {
let maxx = Number.MIN_VALUE
let miny = Number.MAX_VALUE
let maxy = Number.MIN_VALUE
- let curwidth = Number.MIN_VALUE
- let curheight = Number.MIN_VALUE
+
+ const isHorizontalAlign = (type) => ['l', 'c', 'r', 'left', 'center', 'right'].includes(type)
+ const isVerticalAlign = (type) => ['t', 'm', 'b', 'top', 'middle', 'bottom'].includes(type)
+
for (let i = 0; i < len; ++i) {
if (!selectedElements[i]) {
break
}
const elem = selectedElements[i]
bboxes[i] = getStrokedBBoxDefaultVisible([elem])
-
- // now bbox is axis-aligned and handles rotation
- switch (relativeTo) {
- case 'smallest':
- if (
- ((type === 'l' ||
- type === 'c' ||
- type === 'r' ||
- type === 'left' ||
- type === 'center' ||
- type === 'right') &&
- (curwidth === Number.MIN_VALUE || curwidth > bboxes[i].width)) ||
- ((type === 't' ||
- type === 'm' ||
- type === 'b' ||
- type === 'top' ||
- type === 'middle' ||
- type === 'bottom') &&
- (curheight === Number.MIN_VALUE || curheight > bboxes[i].height))
- ) {
- minx = bboxes[i].x
- miny = bboxes[i].y
- maxx = bboxes[i].x + bboxes[i].width
- maxy = bboxes[i].y + bboxes[i].height
- curwidth = bboxes[i].width
- curheight = bboxes[i].height
- }
- break
- case 'largest':
- if (
- ((type === 'l' ||
- type === 'c' ||
- type === 'r' ||
- type === 'left' ||
- type === 'center' ||
- type === 'right') &&
- (curwidth === Number.MIN_VALUE || curwidth < bboxes[i].width)) ||
- ((type === 't' ||
- type === 'm' ||
- type === 'b' ||
- type === 'top' ||
- type === 'middle' ||
- type === 'bottom') &&
- (curheight === Number.MIN_VALUE || curheight < bboxes[i].height))
- ) {
- minx = bboxes[i].x
- miny = bboxes[i].y
- maxx = bboxes[i].x + bboxes[i].width
- maxy = bboxes[i].y + bboxes[i].height
- curwidth = bboxes[i].width
- curheight = bboxes[i].height
- }
- break
- default:
- // 'selected'
- if (bboxes[i].x < minx) {
- minx = bboxes[i].x
- }
- if (bboxes[i].y < miny) {
- miny = bboxes[i].y
- }
- if (bboxes[i].x + bboxes[i].width > maxx) {
- maxx = bboxes[i].x + bboxes[i].width
- }
- if (bboxes[i].y + bboxes[i].height > maxy) {
- maxy = bboxes[i].y + bboxes[i].height
- }
- break
- }
- } // loop for each element to find the bbox and adjust min/max
-
- if (relativeTo === 'page') {
- minx = 0
- miny = 0
- maxx = svgCanvas.getContentW()
- maxy = svgCanvas.getContentH()
}
+ // distribute horizontal and vertical align is not support smallest and largest
+ if (['smallest', 'largest'].includes(relativeTo) && ['dh', 'distrib_horiz', 'dv', 'distrib_verti'].includes(type)) {
+ relativeTo = 'selected'
+ }
+
+ switch (relativeTo) {
+ case 'smallest':
+ if (isHorizontalAlign(type) || isVerticalAlign(type)) {
+ const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
+ const minBbox = sortedBboxes[0]
+ minx = minBbox.x
+ miny = minBbox.y
+ maxx = minBbox.x + minBbox.width
+ maxy = minBbox.y + minBbox.height
+ }
+ break
+ case 'largest':
+ if (isHorizontalAlign(type) || isVerticalAlign(type)) {
+ const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
+ const maxBbox = sortedBboxes[bboxes.length - 1]
+ minx = maxBbox.x
+ miny = maxBbox.y
+ maxx = maxBbox.x + maxBbox.width
+ maxy = maxBbox.y + maxBbox.height
+ }
+ break
+ case 'page':
+ minx = 0
+ miny = 0
+ maxx = svgCanvas.getContentW()
+ maxy = svgCanvas.getContentH()
+ break
+ default:
+ // 'selected'
+ minx = Math.min(...bboxes.map(box => box.x))
+ miny = Math.min(...bboxes.map(box => box.y))
+ maxx = Math.max(...bboxes.map(box => box.x + box.width))
+ maxy = Math.max(...bboxes.map(box => box.y + box.height))
+ break
+ } // adjust min/max
+
+ let dx = []
+ let dy = []
+
+ if (['dh', 'distrib_horiz'].includes(type)) { // distribute horizontal align
+ [dx, dy] = _getDistributeHorizontalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
+ } else if (['dv', 'distrib_verti'].includes(type)) { // distribute vertical align
+ [dx, dy] = _getDistributeVerticalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
+ } else { // normal align (top, left, right, ...)
+ [dx, dy] = _getNormalDistances(type, selectedElements, bboxes, minx, maxx, miny, maxy)
+ }
+
+ moveSelectedElements(dx, dy)
+}
+
+/**
+ * Aligns selected elements.
+ * @function module:selected-elem.SvgCanvas#alignSelectedElements
+ * @param {string} type - String with single character indicating the alignment type
+ * @param {"selected"|"largest"|"smallest"|"page"} relativeTo
+ * @returns {void}
+ */
+
+/**
+ * get distribution horizontal distances.
+ * (internal call only)
+ *
+ * @param {string} relativeTo
+ * @param {Element[]} selectedElements - the array with selected DOM elements
+ * @param {module:utilities.BBoxObject} bboxes - bounding box objects
+ * @param {Float} minx - selected area min-x
+ * @param {Float} maxx - selected area max-x
+ * @param {Float} miny - selected area min-y
+ * @param {Float} maxy - selected area max-y
+ * @returns {[Float[],Float[]]} x and y distances array
+ * @private
+ */
+const _getDistributeHorizontalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
+ const dx = []
+ const dy = []
+
+ for (let i = 0; i < selectedElements.length; i++) {
+ dy[i] = 0
+ }
+
+ const bboxesSortedClone = bboxes
+ .slice()
+ .sort((firstBox, secondBox) => {
+ const firstMaxX = firstBox.x + firstBox.width
+ const secondMaxX = secondBox.x + secondBox.width
+
+ if (firstMaxX === secondMaxX) { return 0 } else if (firstMaxX > secondMaxX) { return 1 } else { return -1 }
+ })
+
+ if (relativeTo === 'page') {
+ bboxesSortedClone.unshift({ x: 0, y: 0, width: 0, height: maxy }) // virtual left box
+ bboxesSortedClone.push({ x: maxx, y: 0, width: 0, height: maxy }) // virtual right box
+ }
+
+ const totalWidth = maxx - minx
+ const totalBoxWidth = bboxesSortedClone.map(b => b.width).reduce((w1, w2) => w1 + w2, 0)
+ const space = (totalWidth - totalBoxWidth) / (bboxesSortedClone.length - 1)
+ const _dx = []
+
+ for (let i = 0; i < bboxesSortedClone.length; ++i) {
+ _dx[i] = 0
+
+ if (i === 0) { continue }
+
+ const orgX = bboxesSortedClone[i].x
+ bboxesSortedClone[i].x = bboxesSortedClone[i - 1].x + bboxesSortedClone[i - 1].width + space
+ _dx[i] = bboxesSortedClone[i].x - orgX
+ }
+
+ bboxesSortedClone.forEach((boxClone, idx) => {
+ const orgIdx = bboxes.findIndex(box => box === boxClone)
+ if (orgIdx !== -1) {
+ dx[orgIdx] = _dx[idx]
+ }
+ })
+
+ return [dx, dy]
+}
+
+/**
+ * get distribution vertical distances.
+ * (internal call only)
+ *
+ * @param {string} relativeTo
+ * @param {Element[]} selectedElements - the array with selected DOM elements
+ * @param {module:utilities.BBoxObject} bboxes - bounding box objects
+ * @param {Float} minx - selected area min-x
+ * @param {Float} maxx - selected area max-x
+ * @param {Float} miny - selected area min-y
+ * @param {Float} maxy - selected area max-y
+ * @returns {[Float[],Float[]]} x and y distances array
+ * @private
+ */
+const _getDistributeVerticalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
+ const dx = []
+ const dy = []
+
+ for (let i = 0; i < selectedElements.length; i++) {
+ dx[i] = 0
+ }
+
+ const bboxesSortedClone = bboxes
+ .slice()
+ .sort((firstBox, secondBox) => {
+ const firstMaxY = firstBox.y + firstBox.height
+ const secondMaxY = secondBox.y + secondBox.height
+
+ if (firstMaxY === secondMaxY) { return 0 } else if (firstMaxY > secondMaxY) { return 1 } else { return -1 }
+ })
+
+ if (relativeTo === 'page') {
+ bboxesSortedClone.unshift({ x: 0, y: 0, width: maxx, height: 0 }) // virtual top box
+ bboxesSortedClone.push({ x: 0, y: maxy, width: maxx, height: 0 }) // virtual bottom box
+ }
+
+ const totalHeight = maxy - miny
+ const totalBoxHeight = bboxesSortedClone.map(b => b.height).reduce((h1, h2) => h1 + h2, 0)
+ const space = (totalHeight - totalBoxHeight) / (bboxesSortedClone.length - 1)
+ const _dy = []
+
+ for (let i = 0; i < bboxesSortedClone.length; ++i) {
+ _dy[i] = 0
+
+ if (i === 0) { continue }
+
+ const orgY = bboxesSortedClone[i].y
+ bboxesSortedClone[i].y = bboxesSortedClone[i - 1].y + bboxesSortedClone[i - 1].height + space
+ _dy[i] = bboxesSortedClone[i].y - orgY
+ }
+
+ bboxesSortedClone.forEach((boxClone, idx) => {
+ const orgIdx = bboxes.findIndex(box => box === boxClone)
+ if (orgIdx !== -1) {
+ dy[orgIdx] = _dy[idx]
+ }
+ })
+
+ return [dx, dy]
+}
+
+/**
+ * get normal align distances.
+ * (internal call only)
+ *
+ * @param {string} type
+ * @param {Element[]} selectedElements - the array with selected DOM elements
+ * @param {module:utilities.BBoxObject} bboxes - bounding box objects
+ * @param {Float} minx - selected area min-x
+ * @param {Float} maxx - selected area max-x
+ * @param {Float} miny - selected area min-y
+ * @param {Float} maxy - selected area max-y
+ * @returns {[Float[],Float[]]} x and y distances array
+ * @private
+ */
+const _getNormalDistances = (type, selectedElements, bboxes, minx, maxx, miny, maxy) => {
+ const len = selectedElements.length
const dx = new Array(len)
const dy = new Array(len)
+
for (let i = 0; i < len; ++i) {
if (!selectedElements[i]) {
break
@@ -419,6 +547,7 @@ const alignSelectedElements = (type, relativeTo) => {
const bbox = bboxes[i]
dx[i] = 0
dy[i] = 0
+
switch (type) {
case 'l': // left (horizontal)
case 'left': // left (horizontal)
@@ -446,7 +575,8 @@ const alignSelectedElements = (type, relativeTo) => {
break
}
}
- moveSelectedElements(dx, dy)
+
+ return [dx, dy]
}
/**