Implement horizontal or vertical distribution alignment (#779)

* Implement horizontal or vertical distribution alignment

* fix lint issue and change align svg image
master
mulder 2022-06-23 16:48:56 +09:00 committed by GitHub
parent 9e3a4a9091
commit 0598e8e978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 219 additions and 79 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 51.04 49.47"><defs><style>.cls-1,.cls-3{fill:none;}.cls-2{fill:#fff;stroke:#fff;stroke-linejoin:round;}.cls-2,.cls-3{stroke-linecap:round;stroke-width:2px;}.cls-3{stroke:#f9ba00;stroke-miterlimit:10;}</style></defs><g id="Calque_2" data-name="Calque 2"><g id="Calque_1-2" data-name="Calque 1"><polyline class="cls-1" points="6.8 0 51.04 0 51.04 49.47 0 49.47 0 0 6.8 0"/><rect class="cls-2" height="17" id="svg_3" width="11.59" x="30.16" y="16.23"/><rect class="cls-2" height="24.34" id="svg_1" width="11.59" x="9.29" y="12.56"/><line class="cls-3" x1="36.21" x2="36.21" y1="4.81" y2="44.66"/><line class="cls-3" x1="14.96" x2="14.96" y1="4.81" y2="44.66"/></g></g></svg>

After

Width:  |  Height:  |  Size: 725 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 51.04 49.47"><defs><style>.cls-1,.cls-3{fill:none;}.cls-2{fill:#fff;stroke:#fff;stroke-linejoin:round;}.cls-2,.cls-3{stroke-linecap:round;stroke-width:2px;}.cls-3{stroke:#f9ba00;stroke-miterlimit:10;}</style></defs><g id="Calque_2" data-name="Calque 2"><g id="Calque_1-2" data-name="Calque 1"><polyline class="cls-1" points="6.8 0 51.04 0 51.04 49.47 0 49.47 0 0 6.8 0"/><rect class="cls-2" height="17" width="11.59" x="20.2" y="6.28" transform="rotate(-90 26 14.7767)"/><rect class="cls-2" height="24.34" width="11.59" x="20.21" y="23.48" transform="rotate(-90 26 35.6533)"/><line class="cls-3" x1="26" x2="26" y1="-5.4" y2="34.45" transform="rotate(-90 26 14.5226)"/><line class="cls-3" x1="26" x2="26" y1="15.85" y2="55.7" transform="rotate(-90 26 35.7734)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@ -52,6 +52,10 @@
img-height="22px"></se-list-item>
<se-list-item id="tool_posbottom" value="b" src="align_bottom.svg" title="tools.align_bottom"
img-height="22px"></se-list-item>
<se-list-item id="tool_poshoriz" value="dh" src="align_distrib_horiz.svg" title="tools.align_distrib_horiz"
img-height="22px"></se-list-item>
<se-list-item id="tool_posverti" value="dv" src="align_distrib_verti.svg" title="tools.align_distrib_verti"
img-height="22px"></se-list-item>
</se-list>
</div>
<div class="xy_panel">
@ -79,6 +83,8 @@
<se-button id="tool_align_top" title="tools.align_top" src="align_top.svg"></se-button>
<se-button id="tool_align_middle" title="tools.align_middle" src="align_middle.svg"></se-button>
<se-button id="tool_align_bottom" title="tools.align_bottom" src="align_bottom.svg"></se-button>
<se-button id="tool_align_distrib_horiz" title="tools.align_distrib_horiz" src="align_distrib_horiz.svg"></se-button>
<se-button id="tool_align_distrib_verti" title="tools.align_distrib_verti" src="align_distrib_verti.svg"></se-button>
<se-select id="tool_align_relative" label="tools.relativeTo"
options="tools.selected_objects,tools.largest_object,tools.smallest_object,tools.page"
values="selected::largest::smallest::page">

View File

@ -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))

View File

@ -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]
}
/**