Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Брозовский Максим #250

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2038a71
Basic setup of TagsCloudVisualization project
BMV989 Nov 12, 2024
f2196bf
Added SpiralCloudLayouter.cs, DefaultPointsDistributor.cs with tests
BMV989 Nov 13, 2024
f4ee9e4
added some more tests
BMV989 Nov 13, 2024
58bc738
small naming fixes
BMV989 Nov 13, 2024
38fdceb
small refactoring of CircularLayouter and SpiralPointsGenerator with …
BMV989 Nov 17, 2024
0f7a10d
add Visualizer and pictures
BMV989 Nov 17, 2024
8ff9eb8
small refactoring of Visualizer
BMV989 Nov 17, 2024
0e12a6d
small refactoring of CircularCloudLayouter
BMV989 Nov 17, 2024
eb5be16
added more tests for CircularCloudLayouterTest
BMV989 Nov 17, 2024
009dc35
added tests for RandomExtension
BMV989 Nov 17, 2024
3090cdb
added test for SpiralPointsGeneratorTest
BMV989 Nov 17, 2024
6940721
fix README.md
BMV989 Nov 17, 2024
04abe31
small naming fixes in CircularCloudLayouterTest
BMV989 Nov 17, 2024
ba4e5f7
small fix in CircularCloudLayouterTest
BMV989 Nov 17, 2024
2a33e20
added 3rd task with Saver and small refactoring
BMV989 Nov 17, 2024
ff64502
refactor of Program
BMV989 Nov 17, 2024
9236bae
added tests for RandomExtensionTest
BMV989 Nov 17, 2024
71ae050
small fix in RandomExtensionTest
BMV989 Nov 17, 2024
f2bceec
renaming of Visualizer to TagCloudVisualizer
BMV989 Nov 18, 2024
e6c398f
refactoring of CircularCloudLayouter with its test
BMV989 Nov 18, 2024
c2cb66f
using SkiaSharp everywhere
BMV989 Nov 18, 2024
8a64693
small refactoring of CircularCloudLayouter
BMV989 Nov 18, 2024
5832415
refactoring of CircularCloudLayouterTest
BMV989 Nov 18, 2024
f71a890
another small refactoring of CircularCloudLayouterTest
BMV989 Nov 18, 2024
4f257ec
small refactoring of TagCloudVisualizer
BMV989 Nov 18, 2024
c74256f
migrate to SkiaSharp in RandomExtensionTest
BMV989 Nov 18, 2024
5c25936
migrate to Random.Shared
BMV989 Nov 18, 2024
d7eda0b
added background and re-rendered pictures
BMV989 Nov 18, 2024
6a2f944
remove unnecessary package
BMV989 Nov 18, 2024
1511372
refactor one of the tests in CircularCloudLayouterTest
BMV989 Nov 18, 2024
87ebcc2
small fixes of CircularCloudLayouterTest
BMV989 Nov 18, 2024
8a67c73
refactor Saver
BMV989 Nov 18, 2024
d18367e
small fix in CircularCloudLayouterTest
BMV989 Nov 18, 2024
0b1de9a
small fixes
BMV989 Nov 18, 2024
79f342d
migrate to List
BMV989 Nov 18, 2024
687a820
At last!
BMV989 Nov 18, 2024
901ffe5
small fix in CircularCloudLayouterTest
BMV989 Nov 18, 2024
b8ce60c
another small fix
BMV989 Nov 18, 2024
cdf1d62
small cleanup
BMV989 Nov 18, 2024
dbd9352
remove useless test in RandomExtensionTest
BMV989 Nov 18, 2024
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
47 changes: 47 additions & 0 deletions cs/TagsCloudVisualization/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class CircularCloudLayouter(Point center, IPointsGenerator pointsGenerator) : ICloudLayouter

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ага, то есть я могу сюда засунуть, например, рандомный генератор точек. Тогда круг из прямоугольников у нас не получится, как заявлено в названии

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поправил

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем нам тут такой конструктор? Он, кажется, только усложняет код, т.к отличается от остальных.

{
private readonly List<Rectangle> rectangles = new();

public Point Center => center;
public IEnumerable<Rectangle> Rectangles => rectangles;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем нам это делать свойствами? Пусть полями внутри лежат. А то я могу взять и поменять координату, там же публичный сеттер у точки

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Point - Struct, а значит это не проблема (он immutable), подробнее тут - https://stackoverflow.com/questions/3456554/points-properties-in-net
(как и SKPoint). Мне они нужны конкретно в тестах, потому что удобно понимать где центр и доставать прямоугольники для отрисовки случаев когда тесты падают. Эти свойства никак инкапсуляцию не нарушают, они readonly getters.


public CircularCloudLayouter(Point center) :
this(center, new SpiralPointsGenerator(center, 1d, 0.5d))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В константы

{

}

public CircularCloudLayouter(Point center, double radius, double angleOffset) :
this(center, new SpiralPointsGenerator(center, radius, angleOffset))
{

}

public Rectangle PutNextRectangle(Size rectangleSize)
{
while (true)
{
var rectanglePosition = pointsGenerator.GetNextPoint();
var rectangle = CreateRectangle(rectanglePosition, rectangleSize);

if (rectangles.Any(rectangle.IntersectsWith)) continue;

rectangles.Add(rectangle);

return rectangle;
}
}

public static Rectangle CreateRectangle(Point center, Size rectangleSize) =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я бы тут явно обозначил в названии, что мы не просто создаем прямоугольник, а создаем его с центром в точке center

new(
center.X - rectangleSize.Width / 2,
center.Y - rectangleSize.Height / 2,
rectangleSize.Width,
rectangleSize.Height
);
}

8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/ICloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudVisualization;

public interface ICloudLayouter
{
public Rectangle PutNextRectangle(Size rectangleSize);
}
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/IPointsGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudVisualization;

public interface IPointsGenerator
{
public Point GetNextPoint();
}
32 changes: 32 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Drawing;
using SkiaSharp;

namespace TagsCloudVisualization;

class Program
{
private const int ImageWidth = 2560;
private const int ImageHeight = 1440;

private const int NumberOfRectangles = 50;
private const int MinRectangleSize = 10;
private const int MaxRectangleSize = 50;

private const string ImageDirectory = "../../../imgs";
public static void Main(string[] args)
{
var center = new Point(ImageWidth / 2, ImageHeight / 2);
var cloudLayouter = new CircularCloudLayouter(center);
var randomizer = new Random();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

На будущее:
Рандом - непотокобезопасный. Сейчас это не страшно, т.к код сейчас не асинхронный.
Но все-таки лучше сразу использовать везде Random.Shared - потокобезопасный рандом. Чтобы потом к этому вопросу не возвращаться, когда появится асинхронность

var rectangles = Enumerable
.Range(0, NumberOfRectangles)
.Select(_ =>
cloudLayouter.PutNextRectangle(randomizer.NextSize(MinRectangleSize, MaxRectangleSize)));

var visualizer = new Visualizer(ImageWidth, ImageHeight);
var bitmap = visualizer.VisualizeTagCloud(rectangles);

var saver = new Saver(ImageDirectory);
saver.SaveAsPng(bitmap, $"{NumberOfRectangles}_TagCloud.png");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Говорим, что сохраним в пнг, но неявно требуем, чтобы в пути тоже было пнг. Можем упростим как-то?

}
}
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### 50 прямоугольников
<img src="imgs/50_TagCloud.png">

### 100 прямоугольников
<img src="imgs/100_TagCloud.png">

### 1000 прямоугольников
<img src="imgs/1000_TagCloud.png">
19 changes: 19 additions & 0 deletions cs/TagsCloudVisualization/RandomExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Drawing;

namespace TagsCloudVisualization;

public static class RandomExtension
{
public static Size NextSize(this Random random, int minValue, int maxValue)
{
if (minValue <= 0)
throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be greater than 0");
if (maxValue < minValue)
throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue must be greater than minValue");

return new Size(random.Next(minValue, maxValue), random.Next(minValue, maxValue));
}

public static Point NextPoint(this Random random, int minValue, int maxValue) =>
new (random.Next(minValue, maxValue), random.Next(minValue, maxValue));
}
13 changes: 13 additions & 0 deletions cs/TagsCloudVisualization/Saver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public class Saver(string imageDirectory)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saver чего?

{
public void SaveAsPng(SKBitmap bitmap, string filename)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем нам на каждую директорию плодить объекты? Почему не можем сразу принять FilePath, а не по отдельности все получать с разным контекстом.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Предлагаю посмотреть в сторону Path.GetDirectoryName(path), чтобы нам сразу явно передавали путь до файла, в т.ч относительный

{
Directory.CreateDirectory(imageDirectory);
using var file = File.OpenWrite(Path.Combine(imageDirectory, filename));
bitmap.Encode(SKEncodedImageFormat.Png, 80).SaveTo(file);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Что такое 80? Как считается, откуда берется?

}
}
41 changes: 41 additions & 0 deletions cs/TagsCloudVisualization/SpiralPointsGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class SpiralPointsGenerator : IPointsGenerator
{
private readonly double angleOffset;
private readonly double radius;
private readonly Point start;
private double angle;

public SpiralPointsGenerator(Point start, double radius, double angleOffset)
{
if (radius <= 0)
throw new ArgumentException("radius must be greater than 0");
if (angleOffset <= 0)
throw new ArgumentException("angleOffset must be greater than 0");

this.angleOffset = angleOffset * Math.PI / 180;
this.radius = radius;
this.start = start;
}

public Point GetNextPoint()
{
var nextPoint = GetPointByPolarCords();
angle += angleOffset;
return nextPoint;
}

private Point GetPointByPolarCords()
{
var offsetPerRadian = radius / (2 * Math.PI);
var radiusVector = offsetPerRadian * angle;

var x = (int)Math.Round(radiusVector * Math.Cos(angle) + start.X);
var y = (int)Math.Round(radiusVector * Math.Sin(angle) + start.Y);

return new Point(x, y);
}
}
15 changes: 15 additions & 0 deletions cs/TagsCloudVisualization/TagsCloudVisualization.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.118.0-preview.1.2" />
<PackageReference Include="SkiaSharp.Views.WindowsForms" Version="3.118.0-preview.1.2" />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А для чего превью-версии? Почему стабильную не можем использовать?

Превью версии это фактически тестовый нюгет пакет, там могут быть уязвимости, итоговый пакет может отличаться абстракциями и тд

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не смог задаунгрейдить, понял, в будущем будут инсталить стабильные стараться.
Вот логи:

@ Installing SkiaSharp in TagsCloudVisualization finished (1,803 sec)
[Notification][Install] Install failed (project: TagsCloudVisualization, package: SkiaSharp v2.88.9)
Package restore failed. Rolling back package changes for 'TagsCloudVisualization'.
Package 'OpenTK 3.1.0' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v ... grade: SkiaSharp from 3.118.0-preview.1.2 to 2.88.9. Reference the package directly from the project to select a different version. 
 TagsCloudVisualization -> SkiaSharp.Views.WindowsForms 3.118.0-preview.1.2 -> SkiaSharp (>= 3.118.0-preview.1.2) 
 TagsCloudVisualization -> SkiaSharp (>= 2.88.9)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ааааа, короче там трабла там на ласт стабильной dotnet 6.0, такие дела

</ItemGroup>

</Project>
32 changes: 32 additions & 0 deletions cs/TagsCloudVisualization/Visualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Drawing;
using SkiaSharp;
using SkiaSharp.Views.Desktop;


namespace TagsCloudVisualization;

public class Visualizer

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Визуалайзер чего?

{
private readonly SKBitmap bitmap;
private readonly SKCanvas canvas;

public Visualizer(int width, int height)
{
bitmap = new SKBitmap(width, height);
canvas = new SKCanvas(bitmap);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну смотри. Из-за того, что у тебя этот контекст расшарен, а задавать можно разные наборы прямоугольников, я смог добиться вот такого поведения. Код твоих абстракций я не изменял, если что. Нам правда нужно это в поля выносить?
50_TagCloud

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не нужно


public SKBitmap VisualizeTagCloud(IEnumerable<Rectangle> rectangles)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай тогда везде использовать абстракции из SkiaSharp. А то странно же получается, что где-то System.Drawing, а где то SkiaSharp) Интерфейс изначальный давай тоже мб поменяем?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

При достаточно больших размерах прямоугольников все начинает ломаться

50_TagCloud

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не уверен в чем тут проблема..... вроде размеры не очень большие типа

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не понял что тут надо сделать короче

{
var paint = new SKPaint
{
Color = SKColors.White,
Style = SKPaintStyle.Stroke
};

foreach (var rectangle in rectangles)
canvas.DrawRect(rectangle.ToSKRect(), paint);

return bitmap;
}
}
Binary file added cs/TagsCloudVisualization/imgs/1000_TagCloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/TagsCloudVisualization/imgs/50_TagCloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 119 additions & 0 deletions cs/TagsCloudVisualizationTests/CircularCloudLayouterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Drawing;
using FluentAssertions;
using NUnit.Framework.Interfaces;
using TagsCloudVisualization;

namespace TagsCloudVisualizationTests;

[TestFixture]
[TestOf(typeof(CircularCloudLayouter))]
public class CircularCloudLayouterTest
{
private const string ImagesDirectory = "../../../failedTests";
private readonly Random randomizer = new();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Опять же, рандом не потокобезопасный. Если появятся асинхронные тесты, то придется отлавливать отдельно

private CircularCloudLayouter circularCloudLayouter;

[SetUp]
public void Setup()
{
circularCloudLayouter = TestContext.CurrentContext.Test.Name.Contains("Optimal") ?
CreateCircularCloudLayouterWithOptimalParams() : CreateCircularCloudLayouterWithRandomParams();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вносим неявную конвенцию через ненадежный источник - имя теста. Так делать не стоит

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я хотел Category поюзать, но стало лень)

}

[TearDown]
public void TearDown()
{
var currentContext = TestContext.CurrentContext;
if (currentContext.Result.Outcome.Status != TestStatus.Failed) return;

var layoutSize = GetLayoutSize(circularCloudLayouter.Rectangles.ToList());
var visualizer = new Visualizer(layoutSize.Width, layoutSize.Height);
var bitmap = visualizer.VisualizeTagCloud(circularCloudLayouter.Rectangles);

var saver = new Saver(ImagesDirectory);
var filename = $"{currentContext.Test.Name}.png";
saver.SaveAsPng(bitmap, filename);

TestContext.Out.WriteLine($"Tag cloud visualization saved to file {Path.Combine(ImagesDirectory, filename)}");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TearDown : System.Exception : Unable to allocate pixels for the bitmap.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Странно у меня все тесты рисует...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

на каком такое падает?

Copy link
Author

@BMV989 BMV989 Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

типа вот
Снимок экрана 2024-11-18 в 15 46 54

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

кстати теперь оно ровное)
Снимок экрана 2024-11-18 в 20 19 31

}

[Test]
public void PutNextRectangle_ShouldReturnRectangle()
{
var rectSize = randomizer.NextSize(1, int.MaxValue);

var rect = circularCloudLayouter.PutNextRectangle(rectSize);

rect.Should().BeOfType<Rectangle>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Какойй смысл у этого теста?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

убрал, смысл был проверить тип, что уже и так делает интерфейс

}

[Test]
public void PutNextRectangle_ShouldReturnRectangleAtCenter_WhenFirstInvoked()
{
var rectSize = randomizer.NextSize(1, int.MaxValue);

var actualRect = circularCloudLayouter.PutNextRectangle(rectSize);
var expectedRect = CircularCloudLayouter.CreateRectangle(circularCloudLayouter.Center, rectSize);

actualRect.Should().BeEquivalentTo(expectedRect);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Почему мы тестируем один метод через то, что он вызывает внутри себя? Если в этом методе будет баг, то мы не отловим в тестах этого


[Test]
public void PutNextRectangle_ShouldReturnRectangle_WithCorrectSize()
{
var recSize = randomizer.NextSize(1, int.MaxValue);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

зачем имена сокращать тут?


var actualRect = circularCloudLayouter.PutNextRectangle(recSize);

actualRect.Size.Should().Be(recSize);
}

[Test]
public void PutNextRectangle_ShouldReturnRectangles_WithoutIntersections()
{
var numberOfRectangles = randomizer.Next(100, 300);

var rectangles = Enumerable
.Range(0, numberOfRectangles)
.Select(_ => circularCloudLayouter.PutNextRectangle(randomizer.NextSize(10, 27)))
.ToList();

rectangles.Any(fr => rectangles.Any(sr => fr != sr && fr.IntersectsWith(sr)))
.Should().BeFalse();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Оч запутано, прям вчитываться приходится. Можем упростить?

}

[Test]
public void GeneratedLayout_ShouldHaveHighTightnessAndShapeOfCircularCloud_WithOptimalParams()
{
const double eps = 0.35;
var rectangles = PutRandomRectanglesInLayouter(randomizer.Next(500, 1000));
var layoutSize = GetLayoutSize(rectangles);

var diameterOfCircle = Math.Max(layoutSize.Width, layoutSize.Height);
var areaOfCircle = Math.PI * Math.Pow(diameterOfCircle, 2) / 4;
var areaOfRectangles = (double)rectangles
.Select(r => r.Height * r.Width)
.Sum();
var areaRatio = areaOfCircle / areaOfRectangles;

areaRatio.Should().BeApproximately(1, eps);
}

private CircularCloudLayouter CreateCircularCloudLayouterWithOptimalParams() => new(new Point(0, 0));
private CircularCloudLayouter CreateCircularCloudLayouterWithRandomParams() =>
new(randomizer.NextPoint(-10, 10), randomizer.Next(1, 10), randomizer.Next(1, 10));

private List<Rectangle> PutRandomRectanglesInLayouter(int numberOfRectangles) =>
Enumerable
.Range(0, numberOfRectangles)
.Select(_ => circularCloudLayouter.PutNextRectangle(randomizer.NextSize(10, 27)))
.ToList();

private Size GetLayoutSize(List<Rectangle> rectangles)
{
var layoutWidth = rectangles.Max(r => r.Right) - rectangles.Min(r => r.Left);
var layoutHeight = rectangles.Max(r => r.Top) - rectangles.Min(r => r.Bottom);

return new Size(layoutWidth, layoutHeight);
}
}
Loading