From e3c90978cbd2b32887485c006ece88086f746999 Mon Sep 17 00:00:00 2001 From: Pierre-Charles David Date: Fri, 17 Mar 2023 10:44:46 +0100 Subject: [PATCH] [1831] Add geometric primitives with tests Bug: https://github.com/eclipse-sirius/sirius-components/issues/1831 Signed-off-by: Pierre-Charles David --- .../diagrams/layout/experimental/Anchor.java | 51 ++++ .../diagrams/layout/experimental/Offsets.java | 108 +++++++++ .../layout/experimental/Rectangle.java | 148 ++++++++++++ .../layout/experimental/AnchorTests.java | 82 +++++++ .../layout/experimental/OffsetsTests.java | 91 +++++++ .../layout/experimental/RectangleTests.java | 228 ++++++++++++++++++ .../diagrams/layoutdata/Position.java | 7 + 7 files changed, 715 insertions(+) create mode 100644 packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Anchor.java create mode 100644 packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Offsets.java create mode 100644 packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Rectangle.java create mode 100644 packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/AnchorTests.java create mode 100644 packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/OffsetsTests.java create mode 100644 packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/RectangleTests.java diff --git a/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Anchor.java b/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Anchor.java new file mode 100644 index 0000000000..e9a5b2ca45 --- /dev/null +++ b/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Anchor.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.experimental; + +import org.eclipse.sirius.components.diagrams.layoutdata.Position; +import org.eclipse.sirius.components.diagrams.layoutdata.Size; + +/** + * Possible values for anchoring a box inside a container. + * + * @author pcdavid + */ +public enum Anchor { + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + + MIDDLE_LEFT, + MIDDLE_CENTER, + MIDDLE_RIGHT, + + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT; + + public Position apply(Rectangle container, Size boxSize) { + return switch (this) { + case TOP_LEFT -> container.topLeft(); + case TOP_CENTER -> container.topCenter().translate(-boxSize.width() / 2.0, 0); + case TOP_RIGHT -> container.topRight().translate(-boxSize.width(), 0.0); + + case MIDDLE_LEFT -> container.middleLeft().translate(0.0, -boxSize.height() / 2.0); + case MIDDLE_CENTER -> container.middleCenter().translate(-boxSize.width() / 2.0, -boxSize.height() / 2.0); + case MIDDLE_RIGHT -> container.middleRight().translate(-boxSize.width(), -boxSize.height() / 2.0); + + case BOTTOM_LEFT -> container.bottomLeft().translate(0.0, -boxSize.height()); + case BOTTOM_CENTER -> container.bottomCenter().translate(-boxSize.width() / 2.0, -boxSize.height()); + case BOTTOM_RIGHT -> container.bottomRight().translate(-boxSize.width(), -boxSize.height()); + }; + } +} diff --git a/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Offsets.java b/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Offsets.java new file mode 100644 index 0000000000..cb3c313148 --- /dev/null +++ b/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Offsets.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.experimental; + +/** + * Specifies the offsets in all 4 directions. Can be used to represent a margin, border, or padding. + * + * @author pcdavid + */ +public record Offsets(double top, double bottom, double left, double right) { + + private static final Offsets EMPTY = Offsets.of(0.0); + + public Offsets(double value) { + this(value, value, value, value); + } + + public Offsets(double top, double bottom, double left, double right) { + this.top = checkNonNegative(top, "top"); + this.bottom = checkNonNegative(bottom, "bottom"); + this.left = checkNonNegative(left, "left"); + this.right = checkNonNegative(right, "right"); + } + + public static Offsets of(double value) { + return new Offsets(value); + } + + public static Offsets empty() { + return EMPTY; + } + + public double width() { + return this.left + this.right; + } + + public double height() { + return this.top + this.bottom; + } + + public Offsets combine(Offsets other) { + return new Offsets(this.top + other.top, this.bottom + other.bottom, this.left + other.left, this.right + other.right); + } + + public Builder newOffsets() { + return new Builder(); + } + + private static double checkNonNegative(double value, String name) { + if (value < 0) { + throw new IllegalArgumentException(name + " can not be negative"); + } + return value; + } + + public static class Builder { + private double top; + + private double bottom; + + private double left; + + private double right; + + public Builder vertical(double value) { + return this.top(value).bottom(value); + } + + public Builder horizontal(double value) { + return this.left(value).right(value); + } + + public Builder top(double newTop) { + this.top = checkNonNegative(newTop, "top"); + return this; + } + + public Builder bottom(double newBottom) { + this.bottom = checkNonNegative(newBottom, "bottom"); + return this; + } + + public Builder left(double newLeft) { + this.left = checkNonNegative(newLeft, "left"); + return this; + } + + public Builder right(double newRight) { + this.right = checkNonNegative(newRight, "right"); + return this; + } + + public Offsets build() { + return new Offsets(this.top, this.bottom, this.left, this.right); + } + + } +} diff --git a/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Rectangle.java b/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Rectangle.java new file mode 100644 index 0000000000..b8f9087737 --- /dev/null +++ b/packages/diagrams/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/experimental/Rectangle.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.experimental; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.sirius.components.diagrams.layoutdata.Position; +import org.eclipse.sirius.components.diagrams.layoutdata.Size; + +/** + * Represents a rectangular area. + * + * @author pcdavid + */ +public record Rectangle(double x, double y, double width, double height) { + + public Rectangle(double x, double y, double width, double height) { + if (width < 0) { + throw new IllegalArgumentException("width can not be negative"); + } + if (height < 0) { + throw new IllegalArgumentException("height can not be negative"); + } + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public Rectangle(Position position, Size size) { + this(position.x(), position.y(), size.width(), size.height()); + } + + public Rectangle translate(double dx, double dy) { + return new Rectangle(this.x + dx, this.y + dy, this.width, this.height); + } + + public Rectangle moveTo(Position newPosition) { + return new Rectangle(newPosition.x(), newPosition.y(), this.width, this.height); + } + + public Rectangle moveTo(double newX, double newY) { + return new Rectangle(newX, newY, this.width, this.height); + } + + public Rectangle resize(double newWidth, double newHeight) { + return new Rectangle(this.x, this.y, newWidth, newHeight); + } + + public Rectangle expand(Offsets offsets) { + return new Rectangle(this.x - offsets.left(), this.y - offsets.top(), this.width + offsets.width(), this.height + offsets.height()); + } + + public Rectangle shrink(Offsets offsets) { + return new Rectangle(this.x + offsets.left(), this.y + offsets.top(), this.width - offsets.width(), this.height - offsets.height()); + } + + public Rectangle union(Rectangle... others) { + List rectangles = new ArrayList<>(); + rectangles.add(this); + rectangles.addAll(Arrays.asList(others)); + + double minX = rectangles.stream().map(Rectangle::x).min(Comparator.naturalOrder()).orElse(0.0); + double minY = rectangles.stream().map(Rectangle::y).min(Comparator.naturalOrder()).orElse(0.0); + double maxX = rectangles.stream().map(r -> r.x + r.width).max(Comparator.naturalOrder()).orElse(0.0); + double maxY = rectangles.stream().map(r -> r.y + r.height).max(Comparator.naturalOrder()).orElse(0.0); + + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + public Rectangle intersection(Rectangle other) { + double topleftX = Math.max(this.x, other.x); + double topleftY = Math.max(this.y, other.y); + double bottomrightX = Math.min(this.bottomRight().x(), other.bottomRight().x()); + double bottomrightY = Math.min(this.bottomRight().y(), other.bottomRight().y()); + if (topleftX > bottomrightX || topleftY > bottomrightY) { + return new Rectangle(0, 0, 0, 0); + } else { + return new Rectangle(topleftX, topleftY, bottomrightX - topleftX, bottomrightY - topleftY); + } + } + + public boolean isEmpty() { + return this.width == 0.0 || this.height == 0.0; + } + + public boolean overlaps(Rectangle other) { + return !this.intersection(other).isEmpty(); + } + + public boolean includes(Rectangle other) { + return this.intersection(other).equals(other); + } + + public Size size() { + return new Size(this.width, this.height); + } + + public Position topLeft() { + return new Position(this.x, this.y); + } + + public Position topCenter() { + return this.topLeft().midPoint(this.topRight()); + } + + public Position topRight() { + return new Position(this.x + this.width, this.y); + } + + public Position middleLeft() { + return this.topLeft().midPoint(this.bottomLeft()); + } + + public Position middleCenter() { + return this.topLeft().midPoint(this.bottomRight()); + } + + public Position middleRight() { + return this.topRight().midPoint(this.bottomRight()); + } + + public Position bottomLeft() { + return new Position(this.x, this.y + this.height); + } + + public Position bottomCenter() { + return this.bottomLeft().midPoint(this.bottomRight()); + } + + public Position bottomRight() { + return new Position(this.x + this.width, this.y + this.height); + } + +} diff --git a/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/AnchorTests.java b/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/AnchorTests.java new file mode 100644 index 0000000000..c868318938 --- /dev/null +++ b/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/AnchorTests.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.experimental; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.sirius.components.diagrams.layoutdata.Position; +import org.eclipse.sirius.components.diagrams.layoutdata.Size; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for the box anchoring logic. + * + * @author pcdavid + */ +public class AnchorTests { + + private Rectangle container; + private Size boxSize; + + @BeforeEach + public void setup() { + this.container = new Rectangle(0, 0, 100, 100); + this.boxSize = new Size(10, 10); + } + + @Test + public void testTopLeft() { + assertThat(Anchor.TOP_LEFT.apply(this.container, this.boxSize)).isEqualTo(new Position(0, 0)); + } + + @Test + public void testTopCenter() { + assertThat(Anchor.TOP_CENTER.apply(this.container, this.boxSize)).isEqualTo(new Position(45.0, 0)); + } + + @Test + public void testTopRight() { + assertThat(Anchor.TOP_RIGHT.apply(this.container, this.boxSize)).isEqualTo(new Position(90.0, 0)); + } + + @Test + public void testMiddleLeft() { + assertThat(Anchor.MIDDLE_LEFT.apply(this.container, this.boxSize)).isEqualTo(new Position(0.0, 45.0)); + } + + @Test + public void testMiddleCenter() { + assertThat(Anchor.MIDDLE_CENTER.apply(this.container, this.boxSize)).isEqualTo(new Position(45.0, 45.0)); + } + + @Test + public void testMiddleRight() { + assertThat(Anchor.MIDDLE_RIGHT.apply(this.container, this.boxSize)).isEqualTo(new Position(90.0, 45.0)); + } + + @Test + public void testBottomLeft() { + assertThat(Anchor.BOTTOM_LEFT.apply(this.container, this.boxSize)).isEqualTo(new Position(0.0, 90.0)); + } + + @Test + public void testBottomCenter() { + assertThat(Anchor.BOTTOM_CENTER.apply(this.container, this.boxSize)).isEqualTo(new Position(45.0, 90.0)); + } + + @Test + public void testBottomRight() { + assertThat(Anchor.BOTTOM_RIGHT.apply(this.container, this.boxSize)).isEqualTo(new Position(90.0, 90.0)); + } +} diff --git a/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/OffsetsTests.java b/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/OffsetsTests.java new file mode 100644 index 0000000000..f70e645429 --- /dev/null +++ b/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/OffsetsTests.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.experimental; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link Offsets} utility class. + * + * @author pcdavid + */ +public class OffsetsTests { + + @Test + public void testNegativeValuesRejected() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new Offsets(-1, 0, 0, 0); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new Offsets(0, -1, 0, 0); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new Offsets(0, 0, -1, 0); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new Offsets(0, 0, 0, -1); + }); + } + + @Test + public void testEmptyOffset() { + var empty = Offsets.empty(); + assertThat(empty).isEqualTo(Offsets.of(0.0)); + assertThat(empty).isEqualTo(new Offsets(0.0, 0.0, 0.0, 0.0)); + assertThat(empty.top()).isEqualTo(0.0); + assertThat(empty.bottom()).isEqualTo(0.0); + assertThat(empty.left()).isEqualTo(0.0); + assertThat(empty.right()).isEqualTo(0.0); + } + + @Test + public void testUniformOffsets() { + double value = 5.0; + var offsets = Offsets.of(value); + assertThat(offsets.top()).isEqualTo(value); + assertThat(offsets.bottom()).isEqualTo(value); + assertThat(offsets.left()).isEqualTo(value); + assertThat(offsets.right()).isEqualTo(value); + assertThat(offsets.width()).isEqualTo(2 * value); + assertThat(offsets.height()).isEqualTo(2 * value); + } + + @Test + public void testNonUniformOffsets() { + var offsets = new Offsets(1.0, 2.0, 3.0, 4.0); + assertThat(offsets.top()).isEqualTo(1.0); + assertThat(offsets.bottom()).isEqualTo(2.0); + assertThat(offsets.left()).isEqualTo(3.0); + assertThat(offsets.right()).isEqualTo(4.0); + assertThat(offsets.width()).isEqualTo(7.0); + assertThat(offsets.height()).isEqualTo(3.0); + } + + @Test + public void testCombineWithEmpty() { + var offsets = Offsets.of(5.0); + assertThat(offsets.combine(Offsets.empty())).isEqualTo(offsets); + assertThat(Offsets.empty().combine(offsets)).isEqualTo(offsets); + } + + @Test + public void testCombine() { + var o1 = Offsets.of(3.0); + var o2 = Offsets.of(2.0); + assertThat(o1.combine(o2)).isEqualTo(Offsets.of(5.0)); + assertThat(o2.combine(o1)).isEqualTo(Offsets.of(5.0)); + } +} diff --git a/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/RectangleTests.java b/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/RectangleTests.java new file mode 100644 index 0000000000..20317d8cf8 --- /dev/null +++ b/packages/diagrams/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/experimental/RectangleTests.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.experimental; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link Rectangle} utility class. + * + * @author pcdavid + */ +public class RectangleTests { + + @Test + public void testNegativeWidthRejected() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new Rectangle(0, 0, -1, 10); + }); + } + + @Test + public void testZeroWidthAccepted() { + var rect = new Rectangle(0, 0, 0, 10); + assertThat(rect.width()).isEqualTo(0.0); + } + + @Test + public void testNegativeHeightRejected() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new Rectangle(0, 0, 10, -1); + }); + } + + @Test + public void testZeroHeightAccepted() { + var rect = new Rectangle(0, 0, 10, 0); + assertThat(rect.height()).isEqualTo(0.0); + } + + @Test + public void testTranslateKeepsSize() { + var rect = new Rectangle(0, 0, 10, 10); + assertThat(rect.translate(10, 10)).isEqualTo(new Rectangle(10, 10, 10, 10)); + } + + @Test + public void testMoveToKeepsSize() { + var rect = new Rectangle(0, 0, 10, 10); + assertThat(rect.moveTo(10, 10)).isEqualTo(new Rectangle(10, 10, 10, 10)); + } + + @Test + public void testResizeKeepsPosition() { + var rect = new Rectangle(0, 0, 10, 10); + assertThat(rect.resize(20, 20)).isEqualTo(new Rectangle(0, 0, 20, 20)); + } + + @Test + public void testIsEmpty() { + assertThat(new Rectangle(0, 0, 0, 0)).matches(Rectangle::isEmpty); + // Even with a non-zero position, a rectangle can be empty + assertThat(new Rectangle(5, 5, 0, 0)).matches(Rectangle::isEmpty); + // A rectangle can be empty even with a non-zero width *or* height, what matters is that its area is 0. + assertThat(new Rectangle(5, 5, 10, 0)).matches(Rectangle::isEmpty); + assertThat(new Rectangle(5, 5, 0, 10)).matches(Rectangle::isEmpty); + } + + @Test + public void testUnionWithSelf() { + var r1 = new Rectangle(0, 0, 30, 30); + assertThat(r1.union(r1)).isEqualTo(r1); + } + + @Test + public void testUnionWithIncludedRectangle() { + var r1 = new Rectangle(0, 0, 30, 30); + var r2 = new Rectangle(10, 10, 10, 10); + assertThat(r1.union(r2)).isEqualTo(r1); + } + + @Test + public void testUnionWithEnclosingRectangle() { + var r1 = new Rectangle(0, 0, 30, 30); + var r2 = new Rectangle(10, 10, 10, 10); + assertThat(r2.union(r1)).isEqualTo(r1); + } + + @Test + public void testUnionNonOverlapping() { + var r1 = new Rectangle(0, 0, 10, 10); + var r2 = new Rectangle(20, -50, 10, 10); + var expected = new Rectangle(0, -50, 30, 60); + assertThat(r1.union(r2)).isEqualTo(expected); + } + + @Test + public void testUnionOfMultipleRectangles() { + var rectangles = List.of(new Rectangle(0, 0, 10, 10), new Rectangle(10, 0, 10, 10), new Rectangle(20, 0, 10, 10), new Rectangle(30, 0, 10, 10), new Rectangle(40, 30, 10, 10)); + + assertThat(rectangles.get(0).union(rectangles.toArray(new Rectangle[0]))).isEqualTo(new Rectangle(0, 0, 50, 40)); + } + + @Test + public void testExpandByEmptyOffsetsIsNoOp() { + var rectangle = new Rectangle(10, 10, 10, 10); + assertThat(rectangle.expand(Offsets.empty())).isEqualTo(rectangle); + } + + @Test + public void testShrinkByEmptyOffsetsIsNoOp() { + var rectangle = new Rectangle(10, 10, 10, 10); + assertThat(rectangle.shrink(Offsets.empty())).isEqualTo(rectangle); + } + + @Test + public void testExpandThenShrinkIsNoOp() { + var rectangle = new Rectangle(10, 10, 10, 10); + Offsets offsets = Offsets.of(2.0); + assertThat(rectangle.expand(offsets).shrink(offsets)).isEqualTo(rectangle); + } + + @Test + public void testShrinkThenEpxandIsNoOp() { + var rectangle = new Rectangle(10, 10, 10, 10); + Offsets offsets = Offsets.of(2.0); + assertThat(rectangle.shrink(offsets).expand(offsets)).isEqualTo(rectangle); + } + + @Test + public void testIntersectionWithEmptyIsEmpty() { + Rectangle empty = new Rectangle(0, 0, 0, 0); + assertThat(new Rectangle(10, 10, 20, 20).intersection(empty)).isEqualTo(empty); + assertThat(empty.intersection(new Rectangle(10, 10, 20, 20))).isEqualTo(empty); + } + + @Test + public void testIntersectionWithSelfIsSelf() { + var rectangle = new Rectangle(10, 10, 10, 10); + assertThat(rectangle.intersection(rectangle)).isEqualTo(rectangle); + } + + @Test + public void testPartialIntersection() { + var r1 = new Rectangle(-5, -5, 10, 10); + var r2 = new Rectangle(0, 0, 30, 30); + var r3 = new Rectangle(0, 0, 5, 5); + assertThat(r1.intersection(r2)).isEqualTo(r3); + assertThat(r2.intersection(r1)).isEqualTo(r3); + } + + @Test + public void testEmptyIntersection() { + var r1 = new Rectangle(-5, -5, 10, 10); + var r2 = new Rectangle(20, 20, 30, 30); + Rectangle empty = new Rectangle(0, 0, 0, 0); + assertThat(r1.intersection(r2)).isEqualTo(empty); + assertThat(r2.intersection(r1)).isEqualTo(empty); + } + + @Test + public void testOverlapWithSelf() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.overlaps(rect)).isTrue(); + } + + /** + * Even though the empty rectangle has a position inside another, they are not considered to overlap. + */ + @Test + public void testOverlapWithEmpty() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.overlaps(new Rectangle(25, 25, 0, 0))).isFalse(); + assertThat(new Rectangle(25, 25, 0, 0).overlaps(rect)).isFalse(); + } + + @Test + public void testPartialOverlap() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.overlaps(new Rectangle(30, 30, 30, 30))).isTrue(); + } + + /** + * Two adjacent rectangles which share a border are not considered to overlap. + */ + @Test + public void testTouchingRectangleDoNotOverlap() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.overlaps(rect.moveTo(rect.topRight()))).isFalse(); + assertThat(rect.overlaps(rect.moveTo(rect.bottomLeft()))).isFalse(); + assertThat(rect.overlaps(rect.moveTo(rect.bottomRight()))).isFalse(); + } + + @Test + public void testIncludeSelf() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.includes(rect)).isTrue(); + } + + @Test + public void testIncludeShrinked() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.includes(rect.shrink(Offsets.of(1)))).isTrue(); + assertThat(rect.includes(rect.resize(20, 20))).isTrue(); + } + + @Test + public void testDoesNotIncludeExpanded() { + var rect = new Rectangle(20, 20, 30, 30); + assertThat(rect.includes(rect.expand(Offsets.of(1)))).isFalse(); + assertThat(rect.includes(rect.resize(40, 40))).isFalse(); + } + +} diff --git a/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Position.java b/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Position.java index d1682a67d0..30450c3c36 100644 --- a/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Position.java +++ b/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Position.java @@ -18,4 +18,11 @@ * @author sbegaudeau */ public record Position(double x, double y) { + public Position translate(double dx, double dy) { + return new Position(this.x + dx, this.y + dy); + } + + public Position midPoint(Position other) { + return new Position((this.x + other.x) / 2.0, (this.y + other.y) / 2.0); + } }