Algorithm needed to determine heirarchy of geometric shapes - c#

I've been trying without much luck to develop an algorithm to sort a collection of closed geometric figures based on whether one shape is completely enclosed within the perimeter of another. When completely analyzed, I should end up with a tree structure that defines the hierarchy.
I can take care of the actual comparison, which is whether one shape is completely within the perimeter of another. I'm having difficulty though with the sorting of unorganized input. I suspect that the solution involves binary tree structures and recursive code, which I've never been strong with.
Geometric data will have already been sanitized prior to generating the sorted hierarchy data, so issues like open paths, overlapping, partially overlapping and self-intersecting shouldn't be an issue.
Below is a group of test figures I've been working with that may help to illustrate my question.
As a human, I can see that the yellow shape is not within the blue one, nor is the blue within the yellow. They are both within the green shape, which is within the red... and so on. (Apologies to those who are color blind)
The resultant tree would be as follows:
I'm working in C# but don't figure it's relevant to the question.
Thank you
EDIT 1
A more concise question might be "How do I generate this tree with the correct order?" (given data in no particular order). Is this just your basic textbook "binary search tree insertion" that I'm over-thinking maybe?
EDIT 2
Attempting to convert Norlesh's pseudo-code into c# and tie it into my existing code, I ended up with the following:
// Return list of shapes contained within container contour but no other
private List<NPContour> DirectlyContained(NPContour container, List<NPContour> contours)
{
List<NPContour> result = new List<NPContour>();
foreach (NPContour contour in contours)
{
if (container.Contains(contour))
{
foreach (NPContour other in contours)
{
if (other.Contains(contour))
break;
result.Add(contour);
}
}
}
return result;
}
// Recursively build tree structure with each node being a list of its children
private void BuildTree(NPContourTreeNode2 parent, List<NPContour> contours)
{
List<NPContour> children = DirectlyContained(parent.Contour, contours);
if (children.Count > 0)
{
// TODO: There's probably a faster or more elegant way to do this but this is clear and good enough for now
foreach (NPContour child in children)
{
contours.Remove(child);
parent.Children.Add(new NPContourTreeNode2(child));
}
foreach (NPContourTreeNode2 child in parent.Children)
{
BuildTree(child, contours);
}
}
}
... And the calling code ....
List<NPContour> contours = new List<NPContour>();
List<NPContour> _topLevelContours = new List<NPContour>();
bool contained = false;
foreach (NPChain chain in _chains)
{
if (chain.Closed)
{
NPContour newContour = new NPContour(chain);
contours.Add(newContour);
}
}
//foreach (NPContour contour in contours)
for (int i = 0; i < contours.Count(); i++)
{
contained = false;
foreach (NPContour container in contours)
{
if (container.Contains(contours[i]))
{
contained = true;
continue;
}
}
if (contained == false)
{
_topLevelContours.Add(contours[i]);
contours.Remove(contours[i]);
}
}
foreach (NPContour topLevelContour in _topLevelContours)
{
NPContourTreeNode2 topLevelNode = new NPContourTreeNode2(topLevelContour);
BuildTree(topLevelNode, contours);
}
I'm thinking I must have misinterpreted something in the translation because it isn't working. I'm going to keep plugging away at it but thought I'd post the code here in hopes someone may help point out my error.
Note that there was a discrepancy in the pseudocode in that buildTree didn't return anything, but in the calling code a return value is appended to ... well, I got a bit confused where exactly it was supposed to be going there anyway. I got the general idea of the example but I think there may have been some important points that were lost on me.
So far in my brief debugging, I seem to get more than one top level shape from the example below (whereas there should only be one) and multiples of the various children (something like 55?). I hope to be able to give more debugging information later.

Here is some pseudo code that should achieve what your trying to do:
// return true if shape is enclosed completely inside container
function contains(container, shape);
// return list of shapes contained within container shape but no other.
function directlyContained(container, shapes) {
result = []
for (shape in shapes) {
if (contains(container, shape)) {
// check its not further down hierarchy
for (other in shapes) {
if (contains(other, shape)) {
break // not the top level container
}
result.append(shape)
}
}
}
return result;
}
// recursively build tree structure with each node being a list of its children
// - removes members of shapes list as they are claimed.
function buildTree(parent, shapes) {
children = directlyContained(parent, shapes)
if (children.length > 0) {
shapes.remove(children);
parent.append(children);
for (child in children) { // recall on each child
buildTree(child, shapes);
}
}
}
function findTopLevel(shapes) {
result = []
// find the one or more top level shapes that are not contained
for shape in shapes {
contained = false;
for (container in shapes) {
if (contains(container, shape)) {
contained = true;
continue;
}
}
if (contained = false) {
scene.append(shape);
shapes.remove(shape);
}
}
return result;
}
shapes = <...>; // list initialized with the unsorted shapes
scene = findTopLevel(shapes);
shapes.remove(scene);
for (top in scene) {
buildTree(top, shapes);
}

Related

How can I rename my creature's model's hierarchy without breaking animations?

I am a beginner at Unity in terms of skill so please explain as if you were talking to a child if you can!
PROBLEM
I would like to change these names here:
I would like to rename them for two reasons:
so they are more intelligible
because I am using many different assets from the store and each has a different hierarchy with different names and I want to standardize the names so that I can use the below code to determine which part of the creature's body was shot so that it works for every creature
public void CreatureHit(string bodyPart, GunInfo usedWeapon, float intensity) // for guns
{
usedWeapon.PlayHit(creatureSounds);
if (creatureInfo.healthPoints > 0) // to prevent dead creatures from being shot
{
if ("Torso" == bodyPart || "LeftUpperArm" == bodyPart // if the part that was hit was the arms or torso
|| "RightUpperArm" == bodyPart || "LeftLowerArm" == bodyPart // if the part that was hit was the arms or torso
|| "RightLowerArm" == bodyPart)
{
creatureInfo.healthPoints -= usedWeapon.damage * intensity; // deal standard dmg
if (creatureInfo.healthPoints <= 0)
creatureInfo.deathType = CreatureInfo.BODYSHOT;
}
else if ("Head" == bodyPart) // if the part that was hit was the head
{
creatureInfo.healthPoints -= usedWeapon.damage * 10 * intensity; // deal 10x dmg
audioSource.PlayOneShot(creatureSounds.hitHead, 1);
if (creatureInfo.healthPoints <= 0)
creatureInfo.deathType = CreatureInfo.HEADSHOT;
}
else if ("RightUpperLeg" == bodyPart || "LeftUpperLeg" == bodyPart
|| "RightLowerLeg" == bodyPart || "LeftLowerLeg" == bodyPart)
{
creatureInfo.healthPoints -= usedWeapon.damage / 2 * intensity; // deal half dmg
if (creatureInfo.healthPoints <= 0)
creatureInfo.deathType = CreatureInfo.BODYSHOT;
}
}
}
WHAT I TRIED
I renamed them in the hierarchy but then the animations stopped working. I found an old thread from the Unity forum asking if this was possible in 2015 and the OP was told that it wasn't. There were some later technical replies and I felt overwhelmed so I thought I should just create my own thread.
NOTE: there are multiple dozens of characters each with 10+ animations so ideally I need a very efficient solution.
In general you still can't unfortunately. (At least not that simple see below).
The AnimationClips are based on strings storing the relative path from the Animator to the according GameObject the type of the according component and finally the name of the animated serialized fields and properties.
If any of those change e.g. because you renamed the object or change the hierarchy in general the connection is lost and the animation breaks.
You could implement an editor script method that
goes through the affected Animator (GetComponentInParent) of the object
iterates through all used AnimationClips
iterates through each clips property bindings
redirects the property path accordingly to your renaming
This could look somewhat like this
private static void RenameObject(GameObject gameObject, Animator animator, string newName)
{
if (!gameObject)
{
throw new ArgumentException("No object provided", nameof(gameObject));
}
if (string.IsNullOrWhiteSpace(newName))
{
throw new ArgumentException("Object name may not be empty!", nameof(newName));
}
if (!animator)
{
throw new ArgumentException($"Selected object {gameObject} is not a child of an {nameof(Animator)}!", nameof(gameObject));
}
if (gameObject.transform == animator.transform)
{
return;
}
// get the relative path from the animator root to this object's parent
var path = AnimationUtility.CalculateTransformPath(gameObject.transform.parent, animator.transform);
if (gameObject.transform.parent != animator.transform)
{
path += "/";
}
// then append the old and new names
var oldPath = path + gameObject.name;
var newPath = path + newName;
// get the runtime Animation controller
var controller = animator.runtimeAnimatorController;
// get all clips used by this controller
var clips = controller.animationClips;
var changeableObjects = new List<Object>(clips.Length + 1) { gameObject };
changeableObjects.AddRange(clips);
Undo.RecordObjects(changeableObjects.ToArray(), "Change animated object name");
// Go through all clips
foreach (var clip in clips)
{
var floatBindingInfo = new List<AnimationFloatBindingInfo>();
// Get and store all FLOAT keyframe bindings
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var curve = AnimationUtility.GetEditorCurve(clip, binding);
var curveInfo = new AnimationFloatBindingInfo(binding, curve);
ReplaceBindingPath(curveInfo, oldPath, newPath);
floatBindingInfo.Add(curveInfo);
}
var objectBindingInfos = new List<AnimationObjectBindingInfo>();
// also do the same for all reference keyframe bindings
foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var curve = AnimationUtility.GetObjectReferenceCurve(clip, binding);
var curveInfo = new AnimationObjectBindingInfo(binding, curve);
ReplaceBindingPath(curveInfo, oldPath, newPath);
objectBindingInfos.Add(curveInfo);
}
// a little check to avoid unnecessary work -> are there any affected property curves at all?
if (floatBindingInfo.Count + objectBindingInfos.Count > 0)
{
// Now erase all curves
clip.ClearCurves();
// and assign back the stored ones
AnimationUtility.SetEditorCurves(clip, floatBindingInfo.Select(info => info.Binding).ToArray(), floatBindingInfo.Select(info => info.Curve).ToArray());
AnimationUtility.SetObjectReferenceCurves(clip, objectBindingInfos.Select(info => info.Binding).ToArray(), objectBindingInfos.Select(info => info.Curve).ToArray());
EditorUtility.SetDirty(clip);
}
}
// finally rename the object
gameObject.name = newName;
EditorUtility.SetDirty(gameObject);
}
Since this use case is quite common I took some time to implement an EditorWindow for this. It is still a bit raw but works and supports also undo redo ;) You can find it here
-> Select the object in the Hierarchy -> right click -> "Rename safe for Animator"
You could of course add some shortcut to it etc. that's up to you ;)
Here a little demo of the dialog in action, renaming some nested objects and also performing some undo/redo
However, an alternative in your use case to simply get your code to work with the names as they are might be using tags instead.
As I see it your code is based on three different cases so you could simply have a tag for each like e.g. Head, Arms, Legs and assign and check those accordingly (GameObject.CompareTag) and not touch the names and animations at all.
I have 2 plans.
Create an empty GameObject under the node you want to rename, and attach the collider compoent on it.
CATRigHub001Bone004
└ CATRigHub001Bone004Bone001
└ Rig <-------- Collider
Rename the bone in editor and create a script to automatically rename it to its original name while playing.
public class Rename : MonoBehaviour
{
public string boneName;
[NonSerialized] public string partName;
void Awake()
{
partName = name;
name = boneName;
}
}

How to solve memory problem of a quadtree with moving objects?

I am creating a quadtree for a 2D game in C# to make the rendering faster. Some of the objects (planets) are moving and that is why, I created a method called removeAndUpdate to update the tree by deleting and reinserting the specific object from the leaves (so not from the root) as I have already read in link. Unfortunately, I got System.OutOfMemoryException after some time at the reinsertion part. I am quite sure, that the removeAndUpdate method has some problems.
Maybe it is good to mention, that when a quad is subdivided (in case, when the quad's storing capacity is full), the content will not distributed among the subquads. I made this way to improve some performance. However, I don't think it has some connection with the problem above.
I guess, there is an infinite loop somewhere, which causes to run out of memory. If I insert new objects from the root, there is no problem.
I wrote a comment to the place where the exception occurs.
Here you can see the important parts of the QuadTree class:
public class QuadTree
{
BoundingBox boundary;
int capacity;
List<WorldObject> leafContent;
public QuadTree parent;
bool divided;
public List<QuadTree> childQuadTrees;
public QuadTree(BoundingBox _boundary, int _capacity, QuadTree _parent)
{
this.boundary = _boundary;
this.capacity = _capacity;
this.leafContent = new List<WorldObject>();
this.childQuadTrees = new List<QuadTree>();
this.divided = false;
this.parent = _parent;
}
// Subdividing the quad, if its storing capacity is full.
public void subdivide()
{
// North-East
this.childQuadTrees.Add(new QuadTree(new BoundingBox(new Vector3(boundary.Center.X, 0, boundary.Min.Z),
new Vector3(boundary.Max.X, 0, boundary.Center.Z)), this.capacity, this));
// North-West
this.childQuadTrees.Add(new QuadTree(new BoundingBox(new Vector3(boundary.Min.X, 0, boundary.Min.Z),
new Vector3(boundary.Center.X, 0, boundary.Center.Z)), this.capacity, this));
// South-East
this.childQuadTrees.Add(new QuadTree(new BoundingBox(boundary.Center, boundary.Max), this.capacity, this));
// South-West
this.childQuadTrees.Add(new QuadTree(new BoundingBox(new Vector3(boundary.Min.X, 0, boundary.Center.Z), new Vector3(boundary.Center.X, 0, boundary.Max.Z)), this.capacity, this));
this.divided = true;
}
public void insert(WorldObject wO)
{
// Checking, whether the quad contains the box at all.
if (!this.ContainOrOverlap(wO.BoundingBox))
{
return;
}
// If there is space, insert the box.
if (this.leafContent.Count < this.capacity && this.divided == false)
{
/*This is the instruction, where System.OutOfMemoryException occures*/
this.leafContent.Add(wO); // <-------
return;
}
// If not, subdivide the quad then insert the obj to the subquads.
else
{
if (this.divided == false)
{
this.subdivide();
}
this.childQuadTrees[0].insert(wO);
this.childQuadTrees[1].insert(wO);
this.childQuadTrees[2].insert(wO);
this.childQuadTrees[3].insert(wO);
}
}
/* Here is the method to update the moving objects bounding volume.
It first removes the element from the tree, updates the boundingbox, then reinsert the object from the leaves*/
public void removeAndUpdate(WorldObject obj, Vector3 displacement)
{
if (!this.ContainOrOverlap(obj.BoundingBox))
{
return;
}
if (this.divided)
{
this.childQuadTrees[0].removeAndUpdate(obj, displacement);
this.childQuadTrees[1].removeAndUpdate(obj, displacement);
this.childQuadTrees[2].removeAndUpdate(obj, displacement);
this.childQuadTrees[3].removeAndUpdate(obj, displacement);
}
/* if the obj is found, remove it, the try to reinsert it to its parent. If its parent does not contain it, move upwards in the hierarchy and try again to insert*/
if (leafContent.Contains(obj))
{
this.leafContent.Remove(obj);
QuadTree q = this.parent;
while (q.ContainOrOverlap(obj.BoundingBox) == false)
{
q = q.parent;
}
obj.BoundingBox.Translate(displacement);
q.insert(obj);
}
}
public void query(BoundingBox range, List<WorldObject> resultList)
{
if (!this.ContainOrOverlap(range))
{
return;
}
if (this.divided)
{
this.childQuadTrees[0].query(range, resultList);
this.childQuadTrees[1].query(range, resultList);
this.childQuadTrees[2].query(range, resultList);
this.childQuadTrees[3].query(range, resultList);
}
resultList.AddList(this.leafContent);
}
}
Here is some info from the debug at the break of the exception:

AvalonEdit: Getting Visual Position for IBackgroundRenderer

In my AvalonEdit-based document editor, I am trying to add marker lines to the text view to indicate folds in the document, in a similar way to how Visual Studio joins the start and end of code block braces with dotted lines.
I have something that produces the correct result if the document is scrolled to the very top, but it doesn't update correctly if the document is scrolled down. Specifically, the lines are drawn as if the text view wasn't scrolled at all, and was still at the top of the document. I suspect the problem has something to do with the TextViewPosition and GetVisualPosition lines, but I don't understand how to correctly get the adjusted visual position with scrolling.
(To be clear, I have checked, and the Draw method is being called at the appropriate times to update the background, it's just that scrolling isn't accounted for when it does)
What I have so far, is the following, on a class which implements IBackgroundRenderer:
public void Draw(TextView textView, DrawingContext drawingContext) {
if (textView == null) { throw new ArgumentNullException("textView"); }
if (drawingContext == null) { throw new ArgumentNullException("drawingContext"); }
if (!textView.VisualLinesValid) { return; }
ReadOnlyCollection<VisualLine> visualLines = textView.VisualLines;
if (visualLines.Count == 0) { return; }
foreach (FoldingSection fold in foldingManager.AllFoldings.Where(f => !f.IsFolded)) {
DocumentLine startLine = textView.Document.GetLineByOffset(fold.StartOffset);
ISegment whitespace = TextUtilities.GetLeadingWhitespace(textView.Document, startLine);
if(whitespace.Length == 0) { continue; }
DocumentLine endLine = textView.Document.GetLineByOffset(fold.EndOffset);
TextLocation foldStart = textView.Document.GetLocation(whitespace.EndOffset);
TextLocation foldEnd = textView.Document.GetLocation(textView.Document.GetOffset(endLine.LineNumber, foldStart.Column));
// I am unsure exactly what TextViewPosition is meant to represent, in contrast to TextLocation
TextViewPosition startViewPos = new TextViewPosition(foldStart);
TextViewPosition endViewPos = new TextViewPosition(foldEnd);
// These lines are definitely not returning what I expect
Point foldStartPos = textView.GetVisualPosition(startViewPos, VisualYPosition.LineBottom);
Point foldEndPos = textView.GetVisualPosition(endViewPos, VisualYPosition.LineBottom);
Brush brush = new SolidColorBrush(LineColor);
brush.Freeze();
Pen dashPen = new Pen(brush, 0.5) { DashStyle = new DashStyle(new double[] { 2, 2 }, 0) };
dashPen.Freeze();
// New point created to avoid issues with nested folds causing slanted lines
drawingContext.DrawLine(dashPen, foldStartPos, new Point(foldStartPos.X, foldEndPos.Y));
}
}
The folding for the document is based on whitespace (very similar to Python-style indentation), hence the use of the leading whitespace for finding the column.
In short, how does one get the properly adjusted visual position from the document line number and column?
GetVisualPosition is documented as:
Returns: The position in WPF device-independent pixels relative to the top left corner of the document.
To use it for painting, you'll want to subtract the scroll position from it:
Point foldStartPos = textView.GetVisualPosition(startViewPos, VisualYPosition.LineBottom);
Point foldEndPos = textView.GetVisualPosition(endViewPos, VisualYPosition.LineBottom);
foldStartPos -= textView.ScrollOffset;
foldEndPos -= textView.ScrollOffset;
As for TextLocation vs. TextViewPosition: there are some cases where there are multiple possible locations that map to the same TextLocation.
There might be custom VisualLineElements with a documentLength of 0.
Or maybe word-wrap is enabled and a line is wrapped at a position where there is no space character: then both the end of the first TextLine and the beginning of the second TextLine refer to the same position in the document.
A TextViewPosition carries some extra information that allows distinguishing these cases. This is mostly important for the caret, so that clicking somewhere places the caret in the clicked position; not another equivalent position.

Check to avoid adding multiple items to canvas in WPF/C#

Shape shape = sm.maakEllips();
if (!canvas.Children.Contains(shape))
{
cm.Draw(shape, canvas, locatie);
}
public void Draw(Shape vorm, Canvas canvas, Point locatie)
{
if (vorm.Height <= canvas.Height && vorm.Width <= canvas.Width)
{
Canvas.SetTop(vorm, locatie.Y);
Canvas.SetLeft(vorm, locatie.X);
canvas.Children.Add(vorm);
}
}
So I add a shape to a canvas in the Draw(). Then when I check on this in the upper if clause, I'm still able to add the same shape to the same canvas multiple times.
I don't get it, what am I doing wrong?
EDIT:
Shape shape = sm.makeShape(Convert.ToByte(textboxR.Text), Convert.ToByte(textboxG.Text), Convert.ToByte(textboxB.Text), Convert.ToInt32(textboxHoogte.Text), Convert.ToInt32(textboxBreedte.Text));
foreach (Shape existingShape in canvas.Children.OfType<Shape>())
{
if (existingShape.Width != shape.Width && existingShape.Height != shape.Height
&& existingShape.Fill != shape.Fill)
{
cm.Draw(shape, canvas, locatie);
}
}
I tried this and now I'm not even able to add a shape to the canvas at all.
I don't see what I'm doing wrong at all.
Your Draw() method add vorm of type Shape to the canvas specified in canvas. And I assume your sm.maakEllips() returns an ellipse.
Therefore, when you run the following code:
Shape shape = sm.maakEllips();
if (!canvas.Children.Contains(shape))
{
cm.Draw(shape, canvas, locatie);
}
You will go inside the if statement only if the canvas contains that exact shape object you created in the line above, using sm.maakEllips() method. It cannot be any shape that has the same properties of the shape object above. Because, every time you create a new object, even with the exact same properties including its name, they are still two distinct objects in .NET world.
To illustrate the point see the code sample below.
Your unchanged Draw() method:
public void Draw(Shape vorm, Canvas canvas, Point locatie)
{
if (vorm.Height <= canvas.Height && vorm.Width <= canvas.Width)
{
Canvas.SetTop(vorm, locatie.Y);
Canvas.SetLeft(vorm, locatie.X);
canvas.Children.Add(vorm);
}
}
A makeEllipse() method that creates an ellipse of width and height of 100 and 80 respectively, and assigns the name passed in the parameter.
public Shape makeEllipse(string name)
{
Shape sh = new Ellipse
{
Name = name,
Width = 100,
Height = 80,
};
return sh;
}
Now see the following code, executed at the click of a button.
private void btnGO_Click(object sender, RoutedEventArgs e)
{
// Creates an ellipse with name "Shape1", and assigns to sh1.
Shape sh1 = makeEllipse("Shape1");
// Adds the said sh1 to the canvas using `Draw()` method.
Draw(sh1, myCanvas, new Point(5, 5));
// See if sh1 exists as a child of `myCanvas`.
// Since sh1 is now a child of canvas, code does NOT go inside the if-clause.
if (!myCanvas.Children.Contains(sh1))
{
Draw(sh1, myCanvas, new Point(5, 5));
}
// Creates an ellipse with the same name "Shape1", and assigns to sh2.
Shape sh2 = makeEllipse("Shape1");
// It is NOT added to the canvas using `Draw()` method.
// Now, here, code DOES go inside the if-clause, because the said object does not exist as a child of `myCanvas`.
if (!myCanvas.Children.Contains(sh2))
{
Draw(sh2, myCanvas, new Point(5, 5));
}
}
Comments above should be good enough, but to explain again,
When you create sh1 and adds it to myCanvas using Draw() method, it becomes a child element of myCanvas.Children.
Then, when you check if it is a child using if (!myCanvas.Children.Contains(sh1)), since it IS a child element by that time, condition becomes false and we do not go inside the if clause.
Next, we create sh2, which has the exact same dimensions and the name as sh1. However, and this is the key, .NET treats it as a different object even though it has the same properties as the previous object. Reason being, whenever we use the new keyword, .NET creates an actual new object.
Afterwards, we DON'T add it to the canvas using Draw() method.
Now, at the second if when we check if myCanvas contains the object, it finds that sh2 is NOT a child of myCanvas, so it goes inside the if clause.
maakEllips() always creates a new Shape. If you want to compare this one against other Shape elements in the Canvas, you need to iterate through these. The following code compares the height, width and position and adds the new Shape to the Canvas if any of them differ:
Shape shape = sm.maakEllips();
foreach (Shape existingShape in canvas.Children.OfType<Shape>())
{
if(existingShape.Width != canvas.Width || existingShape.Height != canvas.Height
|| Canvas.GetLeft(existingShape) != Canvas.GetLeft(shape)
|| Canvas.GetTop(existingShape) != Canvas.GetTop(shape))
{
cm.Draw(shape, canvas, locatie);
}
}

Using MSTest and Fakes (Shim) to shim the .Net System.Windows.Forms.Screen constructor for unit testing

What i am doing
I have written a static extension method that finds all Screen instances that reside to the left/right/above/below the current screen instance.
/// <summary>Finds all screens in the specified directions.</summary>
/// <param name="source">The screen to search around.</param>
/// <param name="directions">The directions to search in.</param>
/// <param name="excludeScreens">Any number of screens to exclude. The source screen is always excluded.</param>
/// <returns>A <see cref="T:Collection{T}"/> of <see cref="Screen"/> containing the found screens.</returns>
public static Collection<Screen> FindAll(this Screen source, ScreenSearchDirections directions, params Screen[] excludeScreens) {
if (source == null)
throw new ArgumentNullException("source");
// Always exclude the source screen.
if (excludeScreens == null)
excludeScreens = new[] { source };
else if (!excludeScreens.Contains(source))
excludeScreens = new List<Screen>(excludeScreens) { source }.ToArray();
// No direction is any direction.
if (directions == ScreenSearchDirections.None)
directions = ScreenSearchDirections.Any;
var result = new Collection<Screen>();
foreach (var screen in Screen.AllScreens.Where(screen => !excludeScreens.Contains(screen))) {
// These are "else if" because otherwise we might find the same screen twice if our directions search for example left and above and the screen
// satisfies both those conditions.
if (directions.HasFlag(ScreenSearchDirections.Left) && screen.Bounds.Right <= source.Bounds.Left)
result.Add(screen);
else if (directions.HasFlag(ScreenSearchDirections.Right) && screen.Bounds.Left >= source.Bounds.Right)
result.Add(screen);
else if (directions.HasFlag(ScreenSearchDirections.Above) && screen.Bounds.Bottom <= source.Bounds.Top)
result.Add(screen);
else if (directions.HasFlag(ScreenSearchDirections.Below) && screen.Bounds.Top >= source.Bounds.Bottom)
result.Add(screen);
}
return result;
}
Constructive suggestions on the code are of course welcome.
What i need
I am of course unit testing all my code, in this case i couldn't do TDD (Test Driven Development) because I simply can't wrap my head around how this operation should be tested. So i wrote the implementation in the hopes of figuring it out after having written it.
And i still can't wrap my head around this one.
Since the .Net implementation of Screen doesn't have any constructor that would take an IScreen interface, nor is there an IScreen interface to begin with, how would i go at doing the set up for my test where i could spoof that i have... say more than 10 screens/monitors attached to my system in any preferred layout for testing?
I have looked at Microsoft Fakes shim examples but it's still not sinking in.
The question is, how can i fake 10+ screens by overriding the Screen constructor?
As per my implementation, i am only going to need the screen bounds so i don't think i would need worry about the other implementations of the Screen class in .Net. As long as i can replace (shim) the constructor of the screen class to set the bounds field to one i would supply in my setup i would be golden, right?
Barring someone here finds a flaw in my reasoning of course!
N.B, while i appreciate that some people here have differing opinions and views, i would humbly request that you remain humble and formulate your arguments in a constructive manner. If i did something wrong then please tell me how i can fix that wrong.
I have time and time again, while asking questions on the SE network, had people say I am wrong without suggesting how i can become right. Thank you for your consideration.
My solution
After looking at this blog entry (Instantiating Classes with Internal Constructors) i finally believe i got it figured out.
I was scratching my head because there were no constructors to shim seeing as how they were internal.
So yeah, there was no way to create more instances of the Screen object until i realized/was reminded that through reflection. One can create zombies. Uninitialized classes where you then set the values on the instances through reflection. This allows one to set values of private members directly of course, which is exactly what i needed.
In any case, this picture made me realize exactly what it was i was looking for. Prior to seeing it i just felt lost reading yet another page about fakes and tests.
Well, the picture and the heading Yes, you heard me correctly, create an object without calling any constructors.
And the text...
At this point in execution, the zombie object will leap to life, with no soul (or state for that matter).
The first thing you should be concerned about is plugging in some values for the private fields, which will be null and performing any critical rolls the constructor would have.
I strongly recommend studying the constructor of your target object in a tool such as Reflector, before initializing it yourself.
The resultant test method
Note that this is a draft, I intend to re-use the mockup for other tests later on.
I didn't need to change anything in my implementation so that stays the same.
[TestMethod]
public void FindAll() {
// Arrange: Create mock source screen and a bunch of mock screen objects that we will use to override (shim) the Screen.AllScreens property getter.
// TODO: Move this to test class instanciation/setup.
// A collection of 12 rectangles specifying the custom desktop layout to perform testing on. First one representing the primary screen.
// In this list we imagine that all screens have the same DPI and that they are frameless.
// Screens are ordered Primary...Quinternary, those marked ??? have not yet had an 'identifier' assigned to them.
// Screens are named Primary for in front of user, then left of primary, right of primary, above primary and finally below primary. Closest screen to primary is selected.
var screenBounds = new Rectangle[] {
new Rectangle(0, 0, 2560, 1440), // Primary screen. In front of the user.
new Rectangle(-1920, 360, 1920, 1080), // Secondary screen. Immediately left of the Primary screen. Lower edge aligned.
new Rectangle(2560, 0, 2560, 1440), // Tertriary screen. Immediately right of the Primary screen.
new Rectangle(0, -720, 1280, 720), // Quaternary screen. Immediately above the Primary screen, left aligned.
new Rectangle(1280, -720, 1280, 720), // ??? screen. Immediately above the Primary screen, right aligned. (This is side by side with the previous screen)
new Rectangle(0, -2160, 2560, 1440), // ??? screen. Above the Quaternary screen and it's neighbor. Spans both those screens.
new Rectangle(-1920, -920, 960, 1280), // ??? screen. Above the Secondary screen, tilted 90 degrees, left aligned.
new Rectangle(-960, -920, 960, 1280), // ??? screen. Above the Secondary screen, tilted 90 degrees, right aligned. (This is side by side with the previous screen)
new Rectangle(0, 1440, 640, 480), // Quinary screen. Immediately below the Primary screen, left aligned.
new Rectangle(640, 1440, 640, 480), // ??? screen. Immediately right of the Quinary screen and immediately below the Primary screen. (This is side by side with the previous screen)
new Rectangle(1280, 1440, 640, 480), // ??? screen. Immediately below the Primary screen and rigth of the previous screen.
new Rectangle(1920, 1440, 640, 480), // ??? screen. Immediately below the Primary screen and rigth of the previous screen.
};
// Create a bunch of mock Screen objects.
var mockAllScreens = new Screen[12];
var mockScreenBoundsField = typeof(Screen).GetField("bounds", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (mockScreenBoundsField == null)
throw new InvalidOperationException("Couldn't get the 'bounds' field on the 'Screen' class.");
var mockScreenPrimaryField = typeof(Screen).GetField("primary", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (mockScreenPrimaryField == null)
throw new InvalidOperationException("Couldn't get the 'primary' field on the 'Screen' class.");
var mockScreenHMonitorField = typeof(Screen).GetField("hmonitor", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (mockScreenHMonitorField == null)
throw new InvalidOperationException("Couldn't get the 'hmonitor' field on the 'Screen' class.");
// TODO: Currently unused, create a collection of device names to assign from.
var mockScreenDeviceNameField = typeof(Screen).GetField("deviceName", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (mockScreenDeviceNameField == null)
throw new InvalidOperationException("Couldn't get the 'deviceName' field on the 'Screen' class.");
for (var mockScreenIndex = 0; mockScreenIndex < mockAllScreens.Length; mockScreenIndex++) {
// Create an uninitialized Screen object.
mockAllScreens[mockScreenIndex] = (Screen)FormatterServices.GetUninitializedObject(typeof(Screen));
// Set the bounds of the Screen object.
mockScreenBoundsField.SetValue(mockAllScreens[mockScreenIndex], screenBounds[mockScreenIndex]);
// Set the hmonitor of the Screen object. We need this for the 'Equals' method to compare properly.
// We don't need this value to be accurate, only different between screens.
mockScreenHMonitorField.SetValue(mockAllScreens[mockScreenIndex], (IntPtr)mockScreenIndex);
// If this is the first screen, it is also the primary screen in our setup.
if (mockScreenIndex == 0)
mockScreenPrimaryField.SetValue(mockAllScreens[mockScreenIndex], true);
}
// Act: Get all screens left of the primary display.
Collection<Screen> result;
using (ShimsContext.Create()) {
ShimScreen.AllScreensGet = () => mockAllScreens;
result = mockAllScreens[0].FindAll(ScreenSearchDirections.Left);
}
// Assert: Compare the result against the picked elements from our mocked screens.
var expected = new Collection<Screen> { mockAllScreens[1], mockAllScreens[6], mockAllScreens[7] };
CollectionAssert.AreEqual(expected, result);
}
As usual, i would happily take advice on what i could improve on both in my implementation and test method(ology).
Oh and as a bonus, here's what the virtual screen layout looks like, because that needed some sort of validation as well. 1/10th scale.
Marked my own answer as the solution. It works wonders so far. Will let you know if it breaks.
Sorry, I can't write an example with MS Shims because license issue.
The one way I see how to improve implementation is to wrap all low level api.
Use ScreenFactory, insted of use AllScreens property directly:
public class ScreensFactory
{
public List<ScreenBoundsWrapper> GetAllScreens()
{
return Screen.AllScreens
.Select(s => new ScreenBoundsWrapper(s))
.ToList();
}
}
so you can pass everywhere mock with custom logic.
Use ScreenBoundsWrapper insted of use screen directly, so you can feel free to create objects for test cases without real screen.
public class ScreenBoundsWrapper
{
public ScreenBoundsWrapper()
{
}
public ScreenBoundsWrapper(Screen screen)
{
screenInstance = screen;
}
public Screen ScreenInstance
{
get { return screenInstance; }
}
public virtual Rectangle Bounds
{
get { return ScreenInstance.Bounds; }
}
public override bool Equals(object obj)
{
var w = obj as ScreenBoundsWrapper;
if (w != null)
{
return w.ScreenInstance.Equals(screenInstance);
}
return obj.Equals(this);
}
protected bool Equals(ScreenBoundsWrapper other)
{
return Equals(screenInstance, other.screenInstance);
}
public override int GetHashCode()
{
return screenInstance == null ? 0 : screenInstance.GetHashCode();
}
private readonly Screen screenInstance;
}
I change your extension method for example:
public static class Extensions
{
public static Collection<ScreenBoundsWrapper> FindAllScreens(
this ScreenBoundsWrapper source,
ScreensFactory factory,
ScreenSearchDirections directions,
params ScreenBoundsWrapper[] excludeScreens)
{
if (source == null)
throw new ArgumentNullException("source");
// Always exclude the source screen.
if (excludeScreens == null)
excludeScreens = new[] { source };
else if (!excludeScreens.Contains(source))
excludeScreens = new List<ScreenBoundsWrapper>(excludeScreens) { source }.ToArray();
// No direction is any direction.
if (directions == ScreenSearchDirections.None)
directions = ScreenSearchDirections.Any;
var allScreens = factory.GetAllScreens();
allScreens.RemoveAll(excludeScreens.Contains);
var result = new Collection<ScreenBoundsWrapper>();
foreach (var screenWraper in allScreens)
{
// These are "else if" because otherwise we might find the same screen twice if our directions search for example left and above and the screen
// satisfies both those conditions.
if (directions.HasFlag(ScreenSearchDirections.Left) && screenWraper.Bounds.Right <= source.Bounds.Left)
result.Add(screenWraper);
else if (directions.HasFlag(ScreenSearchDirections.Right) && screenWraper.Bounds.Left >= source.Bounds.Right)
result.Add(screenWraper);
else if (directions.HasFlag(ScreenSearchDirections.Above) && screenWraper.Bounds.Bottom >= source.Bounds.Top)
result.Add(screenWraper);
else if (directions.HasFlag(ScreenSearchDirections.Below) && screenWraper.Bounds.Top <= source.Bounds.Bottom)
result.Add(screenWraper);
}
return result;
}
And write this test methodin test project (i use moq library):
[TestMethod]
public void TestMethod1()
{
var s1 = MockScreenWraper(1, 1, 1, 1);
var s2 = MockScreenWraper(1, 3, 1, 1);
var list = new List<ScreenBoundsWrapper> { s1, s2 };
var mockScreenFactory = new Mock<ScreensFactory>();
mockScreenFactory
.Setup(m => m.GetAllScreens())
.Returns(() => list);
var factory = mockScreenFactory.Object;
var screenAbove = s1.FindAllScreens(factory, ScreenSearchDirections.Above);
Assert.AreSame(screenAbove.First(), s2);
}
private static ScreenBoundsWrapper MockScreenWraper(int x, int y, int w, int h)
{
var mock = new Mock<ScreenBoundsWrapper>();
mock.SetupGet(m => m.Bounds)
.Returns(() => new Rectangle(x, y, w, h));
mock.Setup(m => m.Equals(It.IsAny<ScreenBoundsWrapper>()))
.Returns<ScreenBoundsWrapper>(
o => ReferenceEquals(mock.Object, o));
return mock.Object;
}
}

Categories

Resources