Browse Source

Implement finding affected chunks for rotated rectangles

Equbuxu 3 years ago
parent
commit
28bb7e2d3d

+ 7 - 0
src/ChunkyImageLib/DataHolders/Vector2d.cs

@@ -53,6 +53,13 @@ namespace ChunkyImageLib.DataHolders
         {
             return Math.Acos((this * other) / Length / other.Length);
         }
+
+        public double CCWAngleTo(Vector2d other)
+        {
+            var rot = other.Rotate(-Angle);
+            return rot.Angle;
+        }
+
         public Vector2d Normalize()
         {
             return new Vector2d(X / Length, Y / Length);

+ 255 - 10
src/ChunkyImageLib/Operations/OperationHelper.cs

@@ -16,28 +16,273 @@ namespace ChunkyImageLib.Operations
         /// <summary>
         /// Finds chunks that at least partially lie inside of a rectangle
         /// </summary>
-        public static HashSet<Vector2i> FindChunksTouchingRectangle(Vector2d center, Vector2d size, float angle, int chunkSize)
+        public static HashSet<Vector2i> FindChunksTouchingRectangle(Vector2d center, Vector2d size, double angle, int chunkSize)
         {
+            if (size.X == 0 || size.Y == 0)
+                return new HashSet<Vector2i>();
+            // draw a line on the outside of each side
             var corners = FindRectangleCorners(center, size, angle);
-            double minX = Math.Min(Math.Min(corners.Item1.X, corners.Item2.X), Math.Min(corners.Item3.X, corners.Item4.X));
-            double maxX = Math.Max(Math.Max(corners.Item1.X, corners.Item2.X), Math.Max(corners.Item3.X, corners.Item4.X));
-            double minY = Math.Min(Math.Min(corners.Item1.Y, corners.Item2.Y), Math.Min(corners.Item3.Y, corners.Item4.Y));
-            double maxY = Math.Max(Math.Max(corners.Item1.Y, corners.Item2.Y), Math.Max(corners.Item3.Y, corners.Item4.Y));
+            List<Vector2i>[] lines = new List<Vector2i>[] {
+                FindChunksAlongLine(corners.Item2, corners.Item1, chunkSize),
+                FindChunksAlongLine(corners.Item3, corners.Item2, chunkSize),
+                FindChunksAlongLine(corners.Item4, corners.Item3, chunkSize),
+                FindChunksAlongLine(corners.Item1, corners.Item4, chunkSize)
+            };
+
+            //find min and max X for each Y in lines
+            var ySel = (Vector2i vec) => vec.Y;
+            int minY = Math.Min(lines[0].Min(ySel), lines[2].Min(ySel));
+            int maxY = Math.Max(lines[0].Max(ySel), lines[2].Max(ySel));
+
+            int[] minXValues = new int[maxY - minY + 1];
+            int[] maxXValues = new int[maxY - minY + 1];
+            for (int i = 0; i < minXValues.Length; i++)
+            {
+                minXValues[i] = int.MaxValue;
+                maxXValues[i] = int.MinValue;
+            }
+
+            for (int i = 0; i < lines.Length; i++)
+            {
+                UpdateMinXValues(lines[i], minXValues, minY);
+                UpdateMaxXValues(lines[i], maxXValues, minY);
+            }
+
+            //draw a line from min X to max X for each Y
+            HashSet<Vector2i> output = new();
+            for (int i = 0; i < minXValues.Length; i++)
+            {
+                int minX = minXValues[i];
+                int maxX = maxXValues[i];
+                for (int x = minX; x <= maxX; x++)
+                    output.Add(new(x, i + minY));
+            }
 
-            //(int leftChunkX, int leftChunkY) = GetChunkPos(minX, )
-            throw new NotImplementedException();
+            return output;
         }
 
+        public static HashSet<Vector2i> FindChunksFullyInsideRectangle(Vector2d center, Vector2d size, double angle, int chunkSize)
+        {
+            if (size.X < chunkSize || size.Y < chunkSize)
+                return new HashSet<Vector2i>();
+            // draw a line on the inside of each side
+            var corners = FindRectangleCorners(center, size, angle);
+            List<Vector2i>[] lines = new List<Vector2i>[] {
+                FindChunksAlongLine(corners.Item1, corners.Item2, chunkSize),
+                FindChunksAlongLine(corners.Item2, corners.Item3, chunkSize),
+                FindChunksAlongLine(corners.Item3, corners.Item4, chunkSize),
+                FindChunksAlongLine(corners.Item4, corners.Item1, chunkSize)
+            };
+
+            //find min and max X for each Y in lines
+            var ySel = (Vector2i vec) => vec.Y;
+            int minY = Math.Min(lines[0].Min(ySel), lines[2].Min(ySel));
+            int maxY = Math.Max(lines[0].Max(ySel), lines[2].Max(ySel));
+
+            int[] minXValues = new int[maxY - minY + 1];
+            int[] maxXValues = new int[maxY - minY + 1];
+            for (int i = 0; i < minXValues.Length; i++)
+            {
+                minXValues[i] = int.MaxValue;
+                maxXValues[i] = int.MinValue;
+            }
+
+            for (int i = 0; i < lines.Length; i++)
+            {
+                UpdateMinXValues(lines[i], minXValues, minY);
+                UpdateMaxXValues(lines[i], maxXValues, minY);
+            }
+
+            //draw a line from min X to max X for each Y and exclude lines
+            HashSet<Vector2i> output = new();
+            for (int i = 0; i < minXValues.Length; i++)
+            {
+                int minX = minXValues[i];
+                int maxX = maxXValues[i];
+                for (int x = minX; x <= maxX; x++)
+                    output.Add(new(x, i + minY));
+            }
+            for (int i = 0; i < lines.Length; i++)
+            {
+                output.ExceptWith(lines[i]);
+            }
+
+            return output;
+        }
+
+        private static void UpdateMinXValues(List<Vector2i> line, int[] minXValues, int minY)
+        {
+            for (int i = 0; i < line.Count; i++)
+            {
+                if (line[i].X < minXValues[line[i].Y - minY])
+                    minXValues[line[i].Y - minY] = line[i].X;
+            }
+        }
+
+        private static void UpdateMaxXValues(List<Vector2i> line, int[] maxXValues, int minY)
+        {
+            for (int i = 0; i < line.Count; i++)
+            {
+                if (line[i].X > maxXValues[line[i].Y - minY])
+                    maxXValues[line[i].Y - minY] = line[i].X;
+            }
+        }
 
-        private static (Vector2d, Vector2d, Vector2d, Vector2d) FindRectangleCorners(Vector2d center, Vector2d size, float angle)
+        /// <summary>
+        /// Think of this function as a line drawing algorithm. 
+        /// The chosen chunks are guaranteed to be on the left side of the line (assuming y going upwards and looking from p1 towards p2).
+        /// This ensures that when you draw a filled shape all updated chunks will be covered (the filled part should go to the right of the line)
+        /// No parts of the line will stick out to the left and be left uncovered
+        /// </summary>
+        public static List<Vector2i> FindChunksAlongLine(Vector2d p1, Vector2d p2, int chunkSize)
+        {
+            if (p1 == p2)
+                return new List<Vector2i>();
+
+            //rotate the line into the first quadrant of the coordinate plane
+            int quadrant;
+            if (p2.X >= p1.X && p2.Y >= p1.Y)
+            {
+                quadrant = 1;
+            }
+            else if (p2.X <= p1.X && p2.Y <= p1.Y)
+            {
+                quadrant = 3;
+                p1 = -p1;
+                p2 = -p2;
+            }
+            else if (p2.X < p1.X)
+            {
+                quadrant = 2;
+                (p1.X, p1.Y) = (p1.Y, -p1.X);
+                (p2.X, p2.Y) = (p2.Y, -p2.X);
+            }
+            else
+            {
+                quadrant = 4;
+                (p1.X, p1.Y) = (-p1.Y, p1.X);
+                (p2.X, p2.Y) = (-p2.Y, p2.X);
+            }
+
+            List<Vector2i> output = new();
+            //vertical line
+            if (p1.X == p2.X)
+            {
+                //if exactly on a chunk boundary, pick the chunk on the top-left
+                Vector2i start = GetChunkPosBiased(p1, false, true, chunkSize);
+                //if exactly on chunk boundary, pick the chunk on the bottom-left
+                Vector2i end = GetChunkPosBiased(p2, false, false, chunkSize);
+                for (int y = start.Y; y <= end.Y; y++)
+                    output.Add(new(start.X, y));
+            }
+            //horizontal line
+            else if (p1.Y == p2.Y)
+            {
+                //if exactly on a chunk boundary, pick the chunk on the top-right
+                Vector2i start = GetChunkPosBiased(p1, true, true, chunkSize);
+                //if exactly on chunk boundary, pick the chunk on the top-left
+                Vector2i end = GetChunkPosBiased(p2, false, true, chunkSize);
+                for (int x = start.X; x <= end.X; x++)
+                    output.Add(new(x, start.Y));
+            }
+            //all other lines
+            else
+            {
+                //y = mx + b
+                double m = (p2.Y - p1.Y) / (p2.X - p1.X);
+                double b = p1.Y - (p1.X * m);
+                Vector2i cur = GetChunkPosBiased(p1, true, true, chunkSize);
+                output.Add(cur);
+                if (LineEq(m, cur.X * chunkSize + chunkSize, b) > cur.Y * chunkSize + chunkSize)
+                    cur.X--;
+                Vector2i end = GetChunkPosBiased(p2, false, false, chunkSize);
+                if (m < 1)
+                {
+                    while (true)
+                    {
+                        if (LineEq(m, cur.X * chunkSize + chunkSize * 2, b) > cur.Y * chunkSize + chunkSize)
+                        {
+                            cur.X++;
+                            cur.Y++;
+                        }
+                        else
+                        {
+                            cur.X++;
+                        }
+                        if (cur.X >= end.X && cur.Y >= end.Y)
+                            break;
+                        output.Add(cur);
+                    }
+                    output.Add(end);
+                }
+                else
+                {
+                    while (true)
+                    {
+                        if (LineEq(m, cur.X * chunkSize + chunkSize, b) <= cur.Y * chunkSize + chunkSize)
+                        {
+                            cur.X++;
+                            cur.Y++;
+                        }
+                        else
+                        {
+                            cur.Y++;
+                        }
+                        if (cur.X >= end.X && cur.Y >= end.Y)
+                            break;
+                        output.Add(cur);
+                    }
+                    output.Add(end);
+                }
+            }
+
+            //rotate output back
+            if (quadrant == 1)
+                return output;
+            if (quadrant == 3)
+            {
+                for (int i = 0; i < output.Count; i++)
+                    output[i] = new(-output[i].X - 1, -output[i].Y - 1);
+                return output;
+            }
+            if (quadrant == 2)
+            {
+                for (int i = 0; i < output.Count; i++)
+                    output[i] = new(-output[i].Y - 1, output[i].X);
+                return output;
+            }
+            for (int i = 0; i < output.Count; i++)
+                output[i] = new(output[i].Y, -output[i].X - 1);
+            return output;
+        }
+
+        private static double LineEq(double m, double x, double b)
+        {
+            return m * x + b;
+        }
+
+        public static Vector2i GetChunkPosBiased(Vector2d pos, bool positiveX, bool positiveY, int chunkSize)
+        {
+            pos /= chunkSize;
+            return new Vector2i()
+            {
+                X = positiveX ? (int)Math.Floor(pos.X) : (int)Math.Ceiling(pos.X) - 1,
+                Y = positiveY ? (int)Math.Floor(pos.Y) : (int)Math.Ceiling(pos.Y) - 1,
+            };
+        }
+
+        /// <summary>
+        /// Returns corners in ccw direction (assuming y points up)
+        /// </summary>
+        private static (Vector2d, Vector2d, Vector2d, Vector2d) FindRectangleCorners(Vector2d center, Vector2d size, double angle)
         {
             Vector2d right = Vector2d.FromAngleAndLength(angle, size.X / 2);
             Vector2d up = Vector2d.FromAngleAndLength(angle + Math.PI / 2, size.Y / 2);
             return (
                 center + right + up,
                 center - right + up,
-                center + right - up,
-                center - right - up
+                center - right - up,
+                center + right - up
                 );
         }
     }

+ 16 - 0
src/ChunkyImageLibTest/OperationHelperTests.cs

@@ -18,5 +18,21 @@ namespace ChunkyImageLibTest
             Assert.Equal(expX, act.X);
             Assert.Equal(expY, act.Y);
         }
+
+        [Theory]
+        [InlineData(0, 0, true, true, 0, 0)]
+        [InlineData(0, 0, false, true, -1, 0)]
+        [InlineData(0, 0, true, false, 0, -1)]
+        [InlineData(0, 0, false, false, -1, -1)]
+        [InlineData(48.5, 48.5, true, true, 1, 1)]
+        [InlineData(48.5, 48.5, false, true, 1, 1)]
+        [InlineData(48.5, 48.5, true, false, 1, 1)]
+        [InlineData(48.5, 48.5, false, false, 1, 1)]
+        public void GetChunkPosBiased_32ChunkSize_ReturnsCorrectValues(double x, double y, bool positiveX, bool positiveY, int expX, int expY)
+        {
+            Vector2i act = OperationHelper.GetChunkPosBiased(new(x, y), positiveX, positiveY, 32);
+            Assert.Equal(expX, act.X);
+            Assert.Equal(expY, act.Y);
+        }
     }
 }

+ 1 - 1
src/ChunkyImageLibTest/RectangleOperationTests.cs

@@ -56,7 +56,7 @@ namespace ChunkyImageLibTest
         [Fact]
         public void FindAffectedChunks_3x3NegativeStrokeOnly_FindsCorrectChunks()
         {
-            var (x, y, w, h) = (-chunkSize * 3 + chunkSize / 2, -chunkSize * 3 + chunkSize / 2, chunkSize * 2, chunkSize * 2);
+            var (x, y, w, h) = (-chunkSize * 3 - chunkSize / 2, -chunkSize * 3 - chunkSize / 2, chunkSize * 2, chunkSize * 2);
             RectangleOperation operation = new(new(new(x, y), new(w, h), 1, SKColors.Black, SKColors.Transparent));
 
             HashSet<Vector2i> expected = new()

+ 9 - 0
src/ChunkyImageLibVis/App.xaml

@@ -0,0 +1,9 @@
+<Application x:Class="ChunkyImageLibVis.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:ChunkyImageLibVis"
+             StartupUri="MainWindow.xaml">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 17 - 0
src/ChunkyImageLibVis/App.xaml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace ChunkyImageLibVis
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 10 - 0
src/ChunkyImageLibVis/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page,
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page,
+                                              // app, or any theme specific resource dictionaries)
+)]

+ 14 - 0
src/ChunkyImageLibVis/ChunkyImageLibVis.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net6.0-windows</TargetFramework>
+    <Nullable>enable</Nullable>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
+  </ItemGroup>
+
+</Project>

+ 16 - 0
src/ChunkyImageLibVis/MainWindow.xaml

@@ -0,0 +1,16 @@
+<Window x:Class="ChunkyImageLibVis.MainWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:ChunkyImageLibVis"
+        mc:Ignorable="d"
+        Title="MainWindow" Height="450" Width="800">
+    <Canvas PreviewMouseDown="Canvas_MouseDown" PreviewMouseMove="Canvas_MouseMove" PreviewMouseUp="Canvas_MouseUp" x:Name="canvas" Background="Transparent">
+        <Rectangle Canvas.Left="{Binding X1}" Canvas.Top="{Binding Y1}" Width="{Binding RectWidth}" Height="{Binding RectHeight}" Stroke="Black" StrokeThickness="1" Panel.ZIndex="999">
+            <Rectangle.RenderTransform>
+                <RotateTransform CenterX="{Binding HalfRectWidth}" CenterY="{Binding HalfRectHeight}" Angle="{Binding Angle}"/>
+            </Rectangle.RenderTransform>
+        </Rectangle>
+    </Canvas>
+</Window>

+ 206 - 0
src/ChunkyImageLibVis/MainWindow.xaml.cs

@@ -0,0 +1,206 @@
+using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace ChunkyImageLibVis
+{
+    /// <summary>
+    /// Interaction logic for MainWindow.xaml
+    /// </summary>
+    public partial class MainWindow : Window, INotifyPropertyChanged
+    {
+        private double x1;
+        private double y1;
+        private double x2;
+        private double y2;
+
+        public double X1
+        {
+            get => x1;
+            set
+            {
+                x1 = value;
+                PropertyChanged?.Invoke(this, new(nameof(X1)));
+                PropertyChanged?.Invoke(this, new(nameof(RectWidth)));
+                PropertyChanged?.Invoke(this, new(nameof(HalfRectWidth)));
+            }
+        }
+        public double X2
+        {
+            get => x2;
+            set
+            {
+                x2 = value;
+                PropertyChanged?.Invoke(this, new(nameof(X2)));
+                PropertyChanged?.Invoke(this, new(nameof(RectWidth)));
+                PropertyChanged?.Invoke(this, new(nameof(HalfRectWidth)));
+            }
+        }
+        public double Y1
+        {
+            get => y1;
+            set
+            {
+                y1 = value;
+                PropertyChanged?.Invoke(this, new(nameof(Y1)));
+                PropertyChanged?.Invoke(this, new(nameof(RectHeight)));
+                PropertyChanged?.Invoke(this, new(nameof(HalfRectHeight)));
+            }
+        }
+        public double Y2
+        {
+            get => y2;
+            set
+            {
+                y2 = value;
+                PropertyChanged?.Invoke(this, new(nameof(Y2)));
+                PropertyChanged?.Invoke(this, new(nameof(RectHeight)));
+                PropertyChanged?.Invoke(this, new(nameof(HalfRectHeight)));
+            }
+        }
+
+        public double RectWidth { get => Math.Abs(X2 - X1); }
+        public double RectHeight { get => Math.Abs(Y2 - Y1); }
+
+        public double HalfRectWidth { get => Math.Abs(X2 - X1) / 2; }
+        public double HalfRectHeight { get => Math.Abs(Y2 - Y1) / 2; }
+
+
+        private double angle;
+        public double Angle
+        {
+            get => angle;
+            set
+            {
+                angle = value;
+                PropertyChanged?.Invoke(this, new(nameof(Angle)));
+            }
+        }
+
+        public MainWindow()
+        {
+            InitializeComponent();
+            DataContext = this;
+            CreateGrid();
+        }
+
+        public void CreateGrid()
+        {
+            for (int i = 0; i < 20; i++)
+            {
+                Line ver = new()
+                {
+                    X1 = i * 32,
+                    X2 = i * 32,
+                    Y1 = 0,
+                    Y2 = 1000,
+                    Stroke = Brushes.Gray,
+                    StrokeThickness = 1,
+                };
+                Line hor = new()
+                {
+                    X1 = 0,
+                    X2 = 1000,
+                    Y1 = i * 32,
+                    Y2 = i * 32,
+                    Stroke = Brushes.Gray,
+                    StrokeThickness = 1,
+                };
+                canvas.Children.Add(ver);
+                canvas.Children.Add(hor);
+            }
+        }
+
+        public List<Rectangle> rectangles = new();
+        private void UpdateChunks()
+        {
+            foreach (var rect in rectangles)
+            {
+                canvas.Children.Remove(rect);
+            }
+            rectangles.Clear();
+            var chunks = OperationHelper.FindChunksTouchingRectangle(new Vector2d(X1 + HalfRectWidth, Y1 + HalfRectHeight), new(X2 - X1, Y2 - Y1), Angle * Math.PI / 180, 32);
+            var innerChunks = OperationHelper.FindChunksFullyInsideRectangle(new Vector2d(X1 + HalfRectWidth, Y1 + HalfRectHeight), new(X2 - X1, Y2 - Y1), Angle * Math.PI / 180, 32);
+            chunks.ExceptWith(innerChunks);
+            foreach (var chunk in chunks)
+            {
+                Rectangle rectangle = new()
+                {
+                    Fill = Brushes.Green,
+                    Width = 32,
+                    Height = 32,
+                };
+                Canvas.SetLeft(rectangle, chunk.X * 32);
+                Canvas.SetTop(rectangle, chunk.Y * 32);
+                canvas.Children.Add(rectangle);
+                rectangles.Add(rectangle);
+            }
+        }
+
+        public event PropertyChangedEventHandler? PropertyChanged;
+
+        private bool drawing = false;
+        private bool rotating = false;
+        private void Canvas_MouseDown(object sender, MouseButtonEventArgs e)
+        {
+            if (rotating)
+            {
+                rotating = false;
+                return;
+            }
+            drawing = true;
+            Angle = 0;
+            var pos = e.GetPosition(canvas);
+            if (e.LeftButton == MouseButtonState.Pressed)
+            {
+                X1 = pos.X;
+                Y1 = pos.Y;
+            }
+            else
+            {
+                X1 = Math.Floor(pos.X / 32) * 32;
+                Y1 = Math.Floor(pos.Y / 32) * 32;
+            }
+        }
+
+        private void Canvas_MouseMove(object sender, MouseEventArgs e)
+        {
+            var pos = e.GetPosition(canvas);
+            if (drawing)
+            {
+                if (e.LeftButton == MouseButtonState.Pressed)
+                {
+                    X2 = pos.X;
+                    Y2 = pos.Y;
+                }
+                else
+                {
+                    X2 = Math.Floor(pos.X / 32) * 32;
+                    Y2 = Math.Floor(pos.Y / 32) * 32;
+                }
+            }
+            else if (rotating)
+            {
+                Vector2d center = new Vector2d(X1 + HalfRectWidth, Y1 + HalfRectHeight);
+                Angle = new Vector2d(pos.X - center.X, pos.Y - center.Y).CCWAngleTo(new Vector2d(X2 - center.X, Y2 - center.Y)) * -180 / Math.PI;
+            }
+            UpdateChunks();
+        }
+
+        private void Canvas_MouseUp(object sender, MouseButtonEventArgs e)
+        {
+            if (drawing)
+            {
+                drawing = false;
+                rotating = true;
+            }
+        }
+    }
+}

+ 7 - 1
src/PixiEditorPrototype.sln

@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLib", "ChunkyIma
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructureRenderer", "StructureRenderer\StructureRenderer.csproj", "{2B396104-7F74-4E03-849E-0AD6EF003666}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChunkyImageLibTest", "ChunkyImageLibTest\ChunkyImageLibTest.csproj", "{794971CA-8CD2-4D1D-BDD9-F41E2638D138}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLibTest", "ChunkyImageLibTest\ChunkyImageLibTest.csproj", "{794971CA-8CD2-4D1D-BDD9-F41E2638D138}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChunkyImageLibVis", "ChunkyImageLibVis\ChunkyImageLibVis.csproj", "{3B0A0186-8AC0-4B1D-8587-4CE4E0E12567}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +41,10 @@ Global
 		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3B0A0186-8AC0-4B1D-8587-4CE4E0E12567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3B0A0186-8AC0-4B1D-8587-4CE4E0E12567}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3B0A0186-8AC0-4B1D-8587-4CE4E0E12567}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3B0A0186-8AC0-4B1D-8587-4CE4E0E12567}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE