Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
8db5e87
Неоконченное проектирование задачи
truefolder Nov 24, 2025
4cbd11c
Добавил класс визуализатора
truefolder Nov 25, 2025
48f1c28
Начальные тесты
truefolder Nov 26, 2025
9b9fee1
Логика спирали и расположения прямоугольников
truefolder Nov 27, 2025
0b27a3b
Фиксы явного преобразования PointF в Point, визуализатор и тест к нему
truefolder Nov 29, 2025
76003dc
Extension для сдвига прямоугольника в центр, вынес конвертацию полярн…
truefolder Nov 29, 2025
1b51945
Сгенерировал несколько раскладок прямоугольников, вложил в проект
truefolder Nov 29, 2025
9e11b80
Добавил TearDown для тестов CircularCloudLayouter, новый тест проверя…
truefolder Nov 30, 2025
895c81f
Разделил тесты ArchimedesSpiral на два разных
truefolder Nov 30, 2025
b1b20a9
Переход с System.Drawing на ImageSharp
truefolder Dec 2, 2025
c7504f8
Утилита для прямоугольников вместо extension-метода
truefolder Dec 2, 2025
5e0e77c
Переименовал метод из ArchimedesSpiral в GetPoints
truefolder Dec 2, 2025
f6b0828
Поправил возвращаемое значение, забыл добавить измененный прямоугольник
truefolder Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions cs/TagCloud/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using SixLabors.ImageSharp;
using TagCloud.CoordinatesProviders;
using TagCloud.Utils;

namespace TagCloud;

public class CircularCloudLayouter(Point center, ICoordinatesProvider coordinatesProvider)
{
public readonly List<Rectangle> Rectangles = [];

public Rectangle PutNextRectangle(Size rectangleSize)
{
if (CheckSizeIncorrectness(rectangleSize))
throw new ArgumentException("Size is incorrect");

var rect = new Rectangle(GetNextRectanglePoint(rectangleSize), rectangleSize);
var shiftedRect = RectangleUtils.ShiftToCenter(rect, center, Rectangles);
Rectangles.Add(shiftedRect);
return shiftedRect;
}

private bool CheckSizeIncorrectness(Size size) =>
size.Width <= 0 || size.Height <= 0;

private Point GetNextRectanglePoint(Size rectangleSize)
{
foreach (var point in coordinatesProvider.GetPoints().Select(Point.Round))
{
var possibleValidPoint = new Point(point.X - rectangleSize.Width / 2, point.Y - rectangleSize.Height / 2);
var possibleValidRectangle = new Rectangle(possibleValidPoint, rectangleSize);
var isIntersects = Rectangles.Any(existingRectangle => possibleValidRectangle.IntersectsWith(existingRectangle));

if (!isIntersects)
return possibleValidPoint;
}

throw new Exception($"Can't find valid point for next rectangle with width: {rectangleSize.Width} and height: {rectangleSize.Height}");
}
}
23 changes: 23 additions & 0 deletions cs/TagCloud/CoordinatesProviders/ArchimedesSpiral.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using SixLabors.ImageSharp;
using TagCloud.Utils;

namespace TagCloud.CoordinatesProviders;

public class ArchimedesSpiral(PointF center, float tightness, float distanceBetweenPoints) : ICoordinatesProvider
{
public IEnumerable<PointF> GetPoints()
{
var degreeStep = distanceBetweenPoints * MathF.PI / 180;
for (var degree = 0f; ; degree += degreeStep)
{
var radius = tightness * degree;

var coords = PolarCoordinatesUtils.ConvertPolarCoordsToCartesian(radius, degree);

coords.x += center.X;
coords.y += center.Y;
yield return new PointF(coords.x, coords.y);
}
// ReSharper disable once IteratorNeverReturns
}
}
8 changes: 8 additions & 0 deletions cs/TagCloud/CoordinatesProviders/ICoordinatesProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using SixLabors.ImageSharp;

namespace TagCloud.CoordinatesProviders;

public interface ICoordinatesProvider
{
public IEnumerable<PointF> GetPoints();
}
Binary file added cs/TagCloud/GeneratedTagClouds/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cs/TagCloud/GeneratedTagClouds/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cs/TagCloud/GeneratedTagClouds/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions cs/TagCloud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
![1.png](GeneratedTagClouds/1.png)

![2.png](GeneratedTagClouds/2.png)

![3.png](GeneratedTagClouds/3.png)
15 changes: 15 additions & 0 deletions cs/TagCloud/TagCloud.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions cs/TagCloud/Utils/PolarCoordinatesUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace TagCloud.Utils;

public static class PolarCoordinatesUtils
{
public static (float x, float y) ConvertPolarCoordsToCartesian(float radius, float degree)
{
var x = radius * MathF.Cos(degree);
var y = radius * MathF.Sin(degree);
return (x, y);
}
}
66 changes: 66 additions & 0 deletions cs/TagCloud/Utils/RectangleUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using SixLabors.ImageSharp;

namespace TagCloud.Utils;

public static class RectangleUtils
{
public static Rectangle ShiftToCenter(Rectangle rectangle, Point center, List<Rectangle> otherRectangles)
{
var canMoveX = true;
var canMoveY = true;

while (canMoveX || canMoveY)
{
canMoveX = TryShiftToCenterByX(rectangle, center, otherRectangles, out var resultedMovementX);
canMoveY = TryShiftToCenterByY(rectangle, center, otherRectangles, out var resultedMovementY);

if (canMoveX)
rectangle.Offset(resultedMovementX);
if (canMoveY)
rectangle.Offset(resultedMovementY);
}

return rectangle;
}

private static bool TryShiftToCenterByX(Rectangle rectangle, Point center, List<Rectangle> otherRectangles, out Point resultedMovement)
{
resultedMovement = new Point(0);

var rectangleCenter = rectangle.X + rectangle.Width / 2;
var directionSign = Math.Sign(center.X - rectangleCenter);

if (directionSign == 0)
return false;

rectangle.X += directionSign;

if (IsIntersectsWithCollection(rectangle, otherRectangles))
return false;

resultedMovement = new Point(directionSign, 0);
return true;
}

private static bool TryShiftToCenterByY(Rectangle rectangle, Point center, List<Rectangle> otherRectangles, out Point resultedMovement)
{
resultedMovement = new Point(0);

var rectangleCenter = rectangle.Y + rectangle.Height / 2;
var directionSign = Math.Sign(center.Y - rectangleCenter);

if (directionSign == 0)
return false;

rectangle.Y += directionSign;

if (IsIntersectsWithCollection(rectangle, otherRectangles))
return false;

resultedMovement = new Point(0, directionSign);
return true;
}

private static bool IsIntersectsWithCollection(Rectangle rectangle, List<Rectangle> otherRectangles) =>
otherRectangles.Any(rectangle.IntersectsWith);
}
50 changes: 50 additions & 0 deletions cs/TagCloud/Visualizers/TagCloudVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

namespace TagCloud.Visualizers;

public class TagCloudVisualizer(Color rectangleColor, Color technicalFiguresColor)
{
public void DrawRectangles(List<Rectangle> rectangles, Size canvasSize, string savePath)
{
var image = new Image<Rgba32>(canvasSize.Width, canvasSize.Height);
var pen = Pens.Dot(rectangleColor, 1);

DrawCenterDot(image, canvasSize);
DrawLimitingCircle(image, canvasSize);

foreach (var rectangle in rectangles)
DrawRectangle(image, pen, new Rectangle(rectangle.Location + canvasSize / 2, rectangle.Size));

image.Save(savePath);
}

private void DrawCenterDot(Image image, SizeF canvasSize)
{
var size = new SizeF(10, 10);
var center = new PointF(canvasSize.Width / 2, canvasSize.Height / 2);
var ellipse = new EllipsePolygon(center, size);

image.Mutate(x => x.Fill(technicalFiguresColor, ellipse));
}

private void DrawLimitingCircle(Image image, SizeF canvasSize)
{
var pen = Pens.Dot(technicalFiguresColor, 1);
var center = new PointF(canvasSize.Width / 2, canvasSize.Height / 2);
var radius = canvasSize.Height / 2;
var ellipse = new EllipsePolygon(center, radius);

image.Mutate(x => x.Draw(pen, ellipse));
}

private void DrawRectangle(Image image, Pen pen, Rectangle rectangle)
{
var rectanglePoly = new RectangularPolygon(rectangle);

image.Mutate(x => x.Draw(pen, rectanglePoly));
}
}
32 changes: 32 additions & 0 deletions cs/TagCloudTests/ArchimedesSpiralTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using FluentAssertions;
using SixLabors.ImageSharp;
using TagCloud.CoordinatesProviders;

namespace TagCloudTests;

public class ArchimedesSpiralTests
{
private ArchimedesSpiral archimedesSpiral;

[Test]
public void GetNextPoint_ShouldReturnPointInCenter_WhenCalledOnce()
{
var center = new PointF(0, 0);
archimedesSpiral = new ArchimedesSpiral(center, 1, 1);

var point = archimedesSpiral.GetPoints().Take(1).ToList();

point.First().Should().Be(center);
}

[Test]
public void GetNextPoint_TwoPointsShouldNotBeSame()
{
var center = new PointF(0, 0);
archimedesSpiral = new ArchimedesSpiral(center, 1, 1);

var point = archimedesSpiral.GetPoints().Take(2).ToList();

point.Last().Should().NotBe(point.First());
}
}
106 changes: 106 additions & 0 deletions cs/TagCloudTests/CircularCloudLayouterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using FluentAssertions;
using NUnit.Framework.Interfaces;
using SixLabors.ImageSharp;
using TagCloud;
using TagCloud.CoordinatesProviders;
using TagCloud.Visualizers;

namespace TagCloudTests;

public class Tests
{
private CircularCloudLayouter layouter;

[SetUp]
public void SetUp()
{
var center = new Point(0, 0);
var provider = new ArchimedesSpiral(center, 1, 1);
layouter = new CircularCloudLayouter(center, provider);
}

[TearDown]
public void TearDown()
{
var testStatus = TestContext.CurrentContext.Result.Outcome.Status;

if (testStatus != TestStatus.Failed)
return;

var testName = TestContext.CurrentContext.Test.Name;
var savePath = $"{AppDomain.CurrentDomain.BaseDirectory}/{testName}failed.png";

var visualizer = new TagCloudVisualizer(Color.Black, Color.Red);
visualizer.DrawRectangles(layouter.Rectangles, new Size(1920, 1080), savePath);

TestContext.Out.WriteLine($"Saved visualization to {savePath})");
}

[TestCaseSource(nameof(GetInvalidSizes))]
public void PutNextRectangle_ShouldThrow_WhenInvalidRectangleSizePresent(Size rectangleSize)
{
var action = () => layouter.PutNextRectangle(rectangleSize);

action.Should().Throw<ArgumentException>();
}

[TestCaseSource(nameof(GetValidSizes))]
public void PutNextRectangle_RectanglesShouldHaveCorrectSizes_WhenValidSizesPresent(Size rectangleSize)
{
var rectangle = layouter.PutNextRectangle(rectangleSize);

rectangle.Width.Should().Be(rectangleSize.Width);
rectangle.Height.Should().Be(rectangleSize.Height);
}

[Test]
public void PutNextRectangle_ShouldNotIntersectWithFirstRectangle_WhenTwoRectanglesAreAlreadyPutted()
{
var rectangle1 = layouter.PutNextRectangle(new Size(10, 10));
var rectangle2 = layouter.PutNextRectangle(new Size(10, 10));

rectangle1.IntersectsWith(rectangle2).Should().BeFalse();
}

[Test]
public void PutNextRectangle_RectanglesShouldNotIntersect_WhenMultipleRectanglesGenerated()
{
var rectangles = new List<Rectangle>();
var random = new Random();

for (var i = 0; i < 100; i++)
rectangles.Add(layouter.PutNextRectangle(new Size(random.Next(10, 100), random.Next(10, 100))));

foreach (var firstRectangle in rectangles)
foreach (var secondRectangle in rectangles.Where(r => firstRectangle != r))
firstRectangle.IntersectsWith(secondRectangle).Should().BeFalse();
}

private static IEnumerable<TestCaseData> GetInvalidSizes()
{
yield return new TestCaseData(new Size(-100, 100))
.SetName("PutNextRectangle_ShouldThrow_WhenWidthIsNegative");
yield return new TestCaseData(new Size(100, -100))
.SetName("PutNextRectangle_ShouldThrow_WhenHeightIsNegative");
yield return new TestCaseData(new Size(-100, -100))
.SetName("PutNextRectangle_ShouldThrow_WhenBothHeightAndWidthAreNegative");
yield return new TestCaseData(new Size(0, 100))
.SetName("PutNextRectangle_ShouldThrow_WhenWidthIsZero");
yield return new TestCaseData(new Size(100, 0))
.SetName("PutNextRectangle_ShouldThrow_WhenHeightIsZero");
yield return new TestCaseData(new Size(-100, 0))
.SetName("PutNextRectangle_ShouldThrow_WhenWidthIsNegativeAndHeightIsZero");
yield return new TestCaseData(new Size(0, -100))
.SetName("PutNextRectangle_ShouldThrow_WhenHeightIsNegativeAndWidthIsZero");
yield return new TestCaseData(new Size(0, 0))
.SetName("PutNextRectangle_ShouldThrow_WhenBothWidthAndHeightAreZero");
}

private static IEnumerable<TestCaseData> GetValidSizes()
{
yield return new TestCaseData(new Size(100, 100));
yield return new TestCaseData(new Size(1, 1));
yield return new TestCaseData(new Size(1, 2));
yield return new TestCaseData(new Size(2, 1));
}
}
Loading