mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
Align and Distribute
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
public enum AlignmentMode {
|
||||
|
||||
LEFT("Align Left"), RIGHT("Align Right"), TOP("Align Top"), BOTTOM("Align Bottom"),
|
||||
HORIZONTAL_CENTER("Align Horizontal Centers"), VERTICAL_CENTER("Align Vertical Centers");
|
||||
|
||||
private String label;
|
||||
|
||||
AlignmentMode(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
public enum DistributionMode {
|
||||
|
||||
HORIZONTAL_CENTERS("Distribute Centers Horizontally"),
|
||||
HORIZONTAL_SPACING("Distribute Spacing Horizontally"),
|
||||
VERTICAL_CENTERS("Distribute Centers Vertically"),
|
||||
VERTICAL_SPACING("Distribute Spacing Vertically");
|
||||
|
||||
private String label;
|
||||
|
||||
DistributionMode(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public boolean isHorizontal() {
|
||||
return this == HORIZONTAL_CENTERS || this == HORIZONTAL_SPACING;
|
||||
}
|
||||
|
||||
public boolean isSpacing() {
|
||||
return this == HORIZONTAL_SPACING || this == VERTICAL_SPACING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,30 @@ public interface ISelectionProcessor extends IGroupProcessor {
|
||||
* @param includeStuckComponents
|
||||
*/
|
||||
void nudgeSelection(Size xOffset, Size yOffset, boolean includeStuckComponents);
|
||||
|
||||
/**
|
||||
* Moves selected components so that their outlines line up as specified. Grouped components move
|
||||
* together as a single unit.
|
||||
*
|
||||
* @param mode
|
||||
*/
|
||||
void alignSelection(AlignmentMode mode);
|
||||
|
||||
/**
|
||||
* Moves selected components so that they are evenly spaced as specified. Grouped components move
|
||||
* together as a single unit.
|
||||
*
|
||||
* @param mode
|
||||
*/
|
||||
void distributeSelection(DistributionMode mode);
|
||||
|
||||
/**
|
||||
* Returns the number of units that align and distribute operate on, where each group counts as
|
||||
* one unit.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int getSelectionUnitCount();
|
||||
|
||||
/**
|
||||
* Selects components that match the criteria.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.common.AlignmentMode;
|
||||
import org.diylc.common.DistributionMode;
|
||||
|
||||
/**
|
||||
* Calculates how far each unit of a selection needs to move to be aligned or distributed. Units are
|
||||
* given by their bounding rectangles and the result holds one delta per rectangle, in input order.
|
||||
* Applying the deltas is left to the caller.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class AlignmentManager {
|
||||
|
||||
public static final int MIN_ALIGN_UNITS = 2;
|
||||
public static final int MIN_DISTRIBUTE_UNITS = 3;
|
||||
|
||||
public List<Point2D> align(List<Rectangle2D> unitBounds, AlignmentMode mode, Double gridStep) {
|
||||
List<Point2D> deltas = createZeroDeltas(unitBounds.size());
|
||||
if (unitBounds.size() < MIN_ALIGN_UNITS) {
|
||||
return deltas;
|
||||
}
|
||||
|
||||
Rectangle2D bounds = new Rectangle2D.Double();
|
||||
bounds.setRect(unitBounds.get(0));
|
||||
for (Rectangle2D r : unitBounds) {
|
||||
bounds.add(r);
|
||||
}
|
||||
|
||||
for (int i = 0; i < unitBounds.size(); i++) {
|
||||
Rectangle2D r = unitBounds.get(i);
|
||||
double dx = 0;
|
||||
double dy = 0;
|
||||
switch (mode) {
|
||||
case LEFT:
|
||||
dx = bounds.getMinX() - r.getMinX();
|
||||
break;
|
||||
case RIGHT:
|
||||
dx = bounds.getMaxX() - r.getMaxX();
|
||||
break;
|
||||
case HORIZONTAL_CENTER:
|
||||
dx = bounds.getCenterX() - r.getCenterX();
|
||||
break;
|
||||
case TOP:
|
||||
dy = bounds.getMinY() - r.getMinY();
|
||||
break;
|
||||
case BOTTOM:
|
||||
dy = bounds.getMaxY() - r.getMaxY();
|
||||
break;
|
||||
case VERTICAL_CENTER:
|
||||
dy = bounds.getCenterY() - r.getCenterY();
|
||||
break;
|
||||
}
|
||||
deltas.get(i).setLocation(snap(dx, gridStep), snap(dy, gridStep));
|
||||
}
|
||||
return deltas;
|
||||
}
|
||||
|
||||
public List<Point2D> distribute(List<Rectangle2D> unitBounds, DistributionMode mode,
|
||||
Double gridStep) {
|
||||
List<Point2D> deltas = createZeroDeltas(unitBounds.size());
|
||||
int count = unitBounds.size();
|
||||
if (count < MIN_DISTRIBUTE_UNITS) {
|
||||
return deltas;
|
||||
}
|
||||
|
||||
boolean horizontal = mode.isHorizontal();
|
||||
// the sort is stable, so units that tie on both keys keep their input order
|
||||
List<Integer> order = new ArrayList<Integer>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
order.add(i);
|
||||
}
|
||||
order.sort(Comparator.<Integer>comparingDouble(i -> getCenter(unitBounds.get(i), horizontal))
|
||||
.thenComparingDouble(i -> getMin(unitBounds.get(i), horizontal)));
|
||||
|
||||
Rectangle2D first = unitBounds.get(order.get(0));
|
||||
Rectangle2D last = unitBounds.get(order.get(count - 1));
|
||||
|
||||
if (mode.isSpacing()) {
|
||||
double totalSize = 0;
|
||||
for (Rectangle2D r : unitBounds) {
|
||||
totalSize += getSize(r, horizontal);
|
||||
}
|
||||
// negative when the units do not fit between the outer edges, which makes them overlap evenly
|
||||
double gap = (getMax(last, horizontal) - getMin(first, horizontal) - totalSize) / (count - 1);
|
||||
double position = getMin(first, horizontal);
|
||||
for (int index : order) {
|
||||
Rectangle2D r = unitBounds.get(index);
|
||||
setDelta(deltas.get(index), position - getMin(r, horizontal), horizontal, gridStep);
|
||||
position += getSize(r, horizontal) + gap;
|
||||
}
|
||||
} else {
|
||||
double firstCenter = getCenter(first, horizontal);
|
||||
double step = (getCenter(last, horizontal) - firstCenter) / (count - 1);
|
||||
for (int k = 0; k < count; k++) {
|
||||
int index = order.get(k);
|
||||
double target = firstCenter + k * step;
|
||||
setDelta(deltas.get(index), target - getCenter(unitBounds.get(index), horizontal),
|
||||
horizontal, gridStep);
|
||||
}
|
||||
}
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static List<Point2D> createZeroDeltas(int count) {
|
||||
List<Point2D> deltas = new ArrayList<Point2D>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
deltas.add(new Point2D.Double());
|
||||
}
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static void setDelta(Point2D delta, double d, boolean horizontal, Double gridStep) {
|
||||
if (horizontal) {
|
||||
delta.setLocation(snap(d, gridStep), 0);
|
||||
} else {
|
||||
delta.setLocation(0, snap(d, gridStep));
|
||||
}
|
||||
}
|
||||
|
||||
private static double snap(double d, Double gridStep) {
|
||||
if (gridStep == null || gridStep <= 0) {
|
||||
return d;
|
||||
}
|
||||
return Math.round(d / gridStep) * gridStep;
|
||||
}
|
||||
|
||||
private static double getMin(Rectangle2D r, boolean horizontal) {
|
||||
return horizontal ? r.getMinX() : r.getMinY();
|
||||
}
|
||||
|
||||
private static double getMax(Rectangle2D r, boolean horizontal) {
|
||||
return horizontal ? r.getMaxX() : r.getMaxY();
|
||||
}
|
||||
|
||||
private static double getCenter(Rectangle2D r, boolean horizontal) {
|
||||
return horizontal ? r.getCenterX() : r.getCenterY();
|
||||
}
|
||||
|
||||
private static double getSize(Rectangle2D r, boolean horizontal) {
|
||||
return horizontal ? r.getWidth() : r.getHeight();
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.swing.JOptionPane;
|
||||
@@ -139,6 +140,7 @@ public class Presenter implements IPlugInPort {
|
||||
private InstantiationManager instantiationManager;
|
||||
private VariantManager variantManager;
|
||||
private BuildingBlockManager buildingBlockManager;
|
||||
private AlignmentManager alignmentManager;
|
||||
|
||||
private Rectangle selectionRect;
|
||||
|
||||
@@ -188,6 +190,7 @@ public class Presenter implements IPlugInPort {
|
||||
instantiationManager = new InstantiationManager();
|
||||
variantManager = new VariantManager(configManager, projectFileManager.getXStream());
|
||||
buildingBlockManager = new BuildingBlockManager(configManager, projectFileManager.getXStream(), instantiationManager);
|
||||
alignmentManager = new AlignmentManager();
|
||||
|
||||
if (importVariantsAndBlocks) {
|
||||
variantManager.upgradeVariants(getComponentTypes());
|
||||
@@ -1112,6 +1115,114 @@ public class Presenter implements IPlugInPort {
|
||||
moveComponents(controlPointMap, dx, dy, false, false);
|
||||
notifyProjectModifiedIfNeeded(oldProject, "Move Selection", true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void alignSelection(AlignmentMode mode) {
|
||||
LOG.debug(String.format("alignSelection(%s)", mode));
|
||||
moveSelectionUnits(mode.toString(),
|
||||
unitBounds -> alignmentManager.align(unitBounds, mode, getAlignmentGridStep()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void distributeSelection(DistributionMode mode) {
|
||||
LOG.debug(String.format("distributeSelection(%s)", mode));
|
||||
moveSelectionUnits(mode.toString(),
|
||||
unitBounds -> alignmentManager.distribute(unitBounds, mode, getAlignmentGridStep()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectionUnitCount() {
|
||||
return findSelectionUnits().size();
|
||||
}
|
||||
|
||||
private void moveSelectionUnits(String action,
|
||||
Function<List<Rectangle2D>, List<Point2D>> deltaCalculator) {
|
||||
// every outline has to be read before the first move, which clears the whole area map
|
||||
List<Set<IDIYComponent<?>>> units = new ArrayList<Set<IDIYComponent<?>>>();
|
||||
List<Rectangle2D> unitBounds = new ArrayList<Rectangle2D>();
|
||||
Set<IDIYComponent<?>> unitComponents = new HashSet<IDIYComponent<?>>();
|
||||
for (Set<IDIYComponent<?>> unit : findSelectionUnits()) {
|
||||
Rectangle2D bounds = getUnitBounds(unit);
|
||||
if (bounds != null) {
|
||||
units.add(unit);
|
||||
unitBounds.add(bounds);
|
||||
unitComponents.addAll(unit);
|
||||
}
|
||||
}
|
||||
List<Point2D> deltas = deltaCalculator.apply(unitBounds);
|
||||
|
||||
Project oldProject = currentProject.clone();
|
||||
for (int i = 0; i < units.size(); i++) {
|
||||
int dx = (int) Math.round(deltas.get(i).getX());
|
||||
int dy = (int) Math.round(deltas.get(i).getY());
|
||||
if (dx == 0 && dy == 0) {
|
||||
continue;
|
||||
}
|
||||
Map<IDIYComponent<?>, Set<Integer>> controlPointMap =
|
||||
new HashMap<IDIYComponent<?>, Set<Integer>>();
|
||||
for (IDIYComponent<?> c : units.get(i)) {
|
||||
if (c.getControlPointCount() > 0) {
|
||||
Set<Integer> pointIndices = new HashSet<Integer>();
|
||||
for (int j = 0; j < c.getControlPointCount(); j++) {
|
||||
pointIndices.add(j);
|
||||
}
|
||||
controlPointMap.put(c, pointIndices);
|
||||
}
|
||||
}
|
||||
if (controlPointMap.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// a component that is a unit of its own must not also be dragged along by another unit,
|
||||
// otherwise it would move twice
|
||||
Set<IDIYComponent<?>> otherUnitComponents = new HashSet<IDIYComponent<?>>(unitComponents);
|
||||
otherUnitComponents.removeAll(units.get(i));
|
||||
includeStuckComponents(controlPointMap, otherUnitComponents);
|
||||
moveComponents(controlPointMap, dx, dy, false, false);
|
||||
}
|
||||
notifyProjectModifiedIfNeeded(oldProject, action, true, true);
|
||||
}
|
||||
|
||||
private List<Set<IDIYComponent<?>>> findSelectionUnits() {
|
||||
List<Set<IDIYComponent<?>>> units = new ArrayList<Set<IDIYComponent<?>>>();
|
||||
if (selectedComponents == null || selectedComponents.isEmpty()) {
|
||||
return units;
|
||||
}
|
||||
Set<IDIYComponent<?>> assigned = new HashSet<IDIYComponent<?>>();
|
||||
// walk the project rather than the selection so that the order of units is deterministic
|
||||
for (IDIYComponent<?> c : currentProject.getComponents()) {
|
||||
if (selectedComponents.contains(c) && !assigned.contains(c)) {
|
||||
Set<IDIYComponent<?>> unit = new HashSet<IDIYComponent<?>>(findAllGroupedComponents(c));
|
||||
unit.retainAll(selectedComponents);
|
||||
assigned.addAll(unit);
|
||||
units.add(unit);
|
||||
}
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
private Rectangle2D getUnitBounds(Set<IDIYComponent<?>> unit) {
|
||||
Rectangle2D bounds = null;
|
||||
for (IDIYComponent<?> c : unit) {
|
||||
ComponentArea area = drawingManager.getComponentArea(c);
|
||||
if (area == null || area.getOutlineArea() == null) {
|
||||
return null;
|
||||
}
|
||||
if (bounds == null) {
|
||||
bounds = area.getOutlineArea().getBounds2D();
|
||||
} else {
|
||||
bounds.add(area.getOutlineArea().getBounds2D());
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private Double getAlignmentGridStep() {
|
||||
String snapTo = configManager.readString(IPlugInPort.SNAP_TO_KEY, IPlugInPort.SNAP_TO_DEFAULT);
|
||||
if (snapTo.equalsIgnoreCase(IPlugInPort.SNAP_TO_GRID)) {
|
||||
return currentProject.getGridSpacing().convertToPixels();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
@@ -1265,6 +1376,11 @@ public class Presenter implements IPlugInPort {
|
||||
* @param controlPointMap
|
||||
*/
|
||||
private void includeStuckComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap) {
|
||||
includeStuckComponents(controlPointMap, Collections.emptySet());
|
||||
}
|
||||
|
||||
private void includeStuckComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap,
|
||||
Set<IDIYComponent<?>> excludedComponents) {
|
||||
int oldSize = controlPointMap.size();
|
||||
LOG.trace("Expanding selected component map");
|
||||
for (IDIYComponent<?> component : currentProject.getComponents()) {
|
||||
@@ -1274,7 +1390,8 @@ public class Presenter implements IPlugInPort {
|
||||
// Do not process a control point if it's already in the map and
|
||||
// if it's locked.
|
||||
if ((!controlPointMap.containsKey(component) || !controlPointMap.get(component).contains(i))
|
||||
&& !isComponentLocked(component) && isComponentVisible(component)) {
|
||||
&& !isComponentLocked(component) && isComponentVisible(component)
|
||||
&& !excludedComponents.contains(component)) {
|
||||
if (component.isControlPointSticky(i)) {
|
||||
boolean componentMatches = false;
|
||||
for (Map.Entry<IDIYComponent<?>, Set<Integer>> entry : controlPointMap.entrySet()) {
|
||||
@@ -1321,7 +1438,7 @@ public class Presenter implements IPlugInPort {
|
||||
// As long as we're adding new components, do another iteration.
|
||||
if (newSize > oldSize) {
|
||||
LOG.trace("Component count changed, trying one more time.");
|
||||
includeStuckComponents(controlPointMap);
|
||||
includeStuckComponents(controlPointMap, excludedComponents);
|
||||
} else {
|
||||
LOG.trace("Component count didn't change, done with expanding.");
|
||||
for (Map.Entry<IDIYComponent<?>, Set<Integer>> entry : controlPointMap.entrySet()) {
|
||||
|
||||
@@ -576,5 +576,21 @@
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>6</major>
|
||||
<minor>8</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-10-09 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Align & Distribute' to the Edit menu and the right-click menu - align the selected components by their left, right, top or bottom edges or their centers, or space them evenly by their centers or by the gaps between them</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
</a>
|
||||
</java.util.Arrays_-ArrayList>
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.common.AlignmentMode;
|
||||
import org.diylc.common.DistributionMode;
|
||||
import org.junit.Test;
|
||||
|
||||
public class AlignmentManagerTest {
|
||||
|
||||
private static final double EPS = 1e-9;
|
||||
|
||||
private AlignmentManager manager = new AlignmentManager();
|
||||
|
||||
private List<Rectangle2D> createUnits() {
|
||||
return Arrays.asList(new Rectangle2D.Double(10, 0, 20, 10),
|
||||
new Rectangle2D.Double(50, 30, 40, 20), new Rectangle2D.Double(0, 100, 10, 40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignLeft() {
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.LEFT, null);
|
||||
assertDelta(-10, 0, deltas.get(0));
|
||||
assertDelta(-50, 0, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignRight() {
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.RIGHT, null);
|
||||
assertDelta(60, 0, deltas.get(0));
|
||||
assertDelta(0, 0, deltas.get(1));
|
||||
assertDelta(80, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignHorizontalCenter() {
|
||||
// the selection spans x = 0..90, so every unit is centered on x = 45
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.HORIZONTAL_CENTER, null);
|
||||
assertDelta(25, 0, deltas.get(0));
|
||||
assertDelta(-25, 0, deltas.get(1));
|
||||
assertDelta(40, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignTop() {
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.TOP, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(0, -30, deltas.get(1));
|
||||
assertDelta(0, -100, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignBottom() {
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.BOTTOM, null);
|
||||
assertDelta(0, 130, deltas.get(0));
|
||||
assertDelta(0, 90, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignVerticalCenter() {
|
||||
// the selection spans y = 0..140, so every unit is centered on y = 70
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.VERTICAL_CENTER, null);
|
||||
assertDelta(0, 65, deltas.get(0));
|
||||
assertDelta(0, 30, deltas.get(1));
|
||||
assertDelta(0, -50, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignAlreadyAligned() {
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(10, 0, 20, 10),
|
||||
new Rectangle2D.Double(10, 50, 5, 10));
|
||||
List<Point2D> deltas = manager.align(units, AlignmentMode.LEFT, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(0, 0, deltas.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignSnapsToGrid() {
|
||||
List<Point2D> deltas = manager.align(createUnits(), AlignmentMode.HORIZONTAL_CENTER, 20d);
|
||||
assertDelta(20, 0, deltas.get(0));
|
||||
assertDelta(-20, 0, deltas.get(1));
|
||||
assertDelta(40, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlignSingleUnitDoesNothing() {
|
||||
List<Point2D> deltas = manager.align(
|
||||
Collections.<Rectangle2D>singletonList(new Rectangle2D.Double(10, 10, 5, 5)),
|
||||
AlignmentMode.LEFT, null);
|
||||
assertEquals(1, deltas.size());
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeCentersHorizontally() {
|
||||
// centers at 5, 15 and 105, given out of order; the outer two stay put
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(100, 0, 10, 10),
|
||||
new Rectangle2D.Double(0, 0, 10, 10), new Rectangle2D.Double(5, 0, 20, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.HORIZONTAL_CENTERS, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(0, 0, deltas.get(1));
|
||||
assertDelta(40, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeCentersVertically() {
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 10, 10),
|
||||
new Rectangle2D.Double(0, 10, 10, 10), new Rectangle2D.Double(0, 20, 10, 10),
|
||||
new Rectangle2D.Double(0, 90, 10, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.VERTICAL_CENTERS, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(0, 20, deltas.get(1));
|
||||
assertDelta(0, 40, deltas.get(2));
|
||||
assertDelta(0, 0, deltas.get(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeSpacingHorizontally() {
|
||||
// widths 10 + 30 + 20 across a span of 0..100 leave two gaps of 20
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 10, 10),
|
||||
new Rectangle2D.Double(15, 0, 30, 10), new Rectangle2D.Double(80, 0, 20, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.HORIZONTAL_SPACING, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(15, 0, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeSpacingVertically() {
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 10, 10),
|
||||
new Rectangle2D.Double(0, 15, 10, 30), new Rectangle2D.Double(0, 80, 10, 20));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.VERTICAL_SPACING, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(0, 15, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeSpacingOverlapsWhenUnitsDoNotFit() {
|
||||
// widths 20 + 40 + 20 across a span of 0..50 leave two gaps of -15
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 20, 10),
|
||||
new Rectangle2D.Double(10, 0, 40, 10), new Rectangle2D.Double(30, 0, 20, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.HORIZONTAL_SPACING, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(-5, 0, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeTiesKeepInputOrder() {
|
||||
// the last two units share both their center and their left edge
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 10, 10),
|
||||
new Rectangle2D.Double(40, 0, 10, 10), new Rectangle2D.Double(40, 0, 10, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.HORIZONTAL_CENTERS, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(-20, 0, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeSnapsToGrid() {
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 10, 10),
|
||||
new Rectangle2D.Double(10, 0, 10, 10), new Rectangle2D.Double(70, 0, 10, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.HORIZONTAL_CENTERS, 20d);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(20, 0, deltas.get(1));
|
||||
assertDelta(0, 0, deltas.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeTwoUnitsDoesNothing() {
|
||||
List<Rectangle2D> units = Arrays.asList(new Rectangle2D.Double(0, 0, 10, 10),
|
||||
new Rectangle2D.Double(70, 0, 10, 10));
|
||||
List<Point2D> deltas = manager.distribute(units, DistributionMode.HORIZONTAL_SPACING, null);
|
||||
assertDelta(0, 0, deltas.get(0));
|
||||
assertDelta(0, 0, deltas.get(1));
|
||||
}
|
||||
|
||||
private static void assertDelta(double dx, double dy, Point2D delta) {
|
||||
assertEquals(dx, delta.getX(), EPS);
|
||||
assertEquals(dy, delta.getY(), EPS);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user