Align and Distribute

This commit is contained in:
Bane Stojkovic
2026-10-09 23:18:02 +02:00
parent cd9ac32a2f
commit 33b614c7ff
29 changed files with 1018 additions and 9 deletions
@@ -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 &amp; 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);
}
}