Skip to content

Commit afba8ab

Browse files
committed
VIX-3830 Improve Prop Overlap Rubberband Selection
VIX-3830 Improve Prop Overlap Rubberband Selection
1 parent 6fd222a commit afba8ab

File tree

3 files changed

+208
-91
lines changed

3 files changed

+208
-91
lines changed

src/Vixen.Application/SetupDisplay/OpenGL/OpenGLSetupPreviewDrawingEngine.cs

Lines changed: 137 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,30 @@ public class OpenGLSetupPreviewDrawingEngine : OpenGLDrawingEngineBase<ILightPro
9393
/// </summary>
9494
private float _rubberBandStartY;
9595

96+
/// <summary>
97+
/// Top left X coordinate in screen coordinates of the normalized rubberband selection rectangle.
98+
/// Normalized such that the ruberband is always oriented left to right, top to bottom.
99+
/// </summary>
100+
private float _normalizedRubberbandPt1X;
101+
102+
/// <summary>
103+
/// Top left Y coordinate in screen coordinates of the normalized rubberband selection rectangle.
104+
/// Normalized such that the ruberband is always oriented left to right, top to bottom.
105+
/// </summary>
106+
private float _normalizedRubberbandPt1Y;
107+
108+
/// <summary>
109+
/// Bottom right X coordinate in screen coordinates of the normalized rubberband selection rectangle.
110+
/// Normalized such that the ruberband is always oriented left to right, top to bottom.
111+
/// </summary>
112+
private float _normalizedRubberbandPt2X;
113+
114+
/// <summary>
115+
/// Bottom right Y coordinate in screen coordinates of the normalized rubberband selection rectangle.
116+
/// Normalized such that the ruberband is always oriented left to right, top to bottom.
117+
/// </summary>
118+
private float _normalizedRubberbandPt2Y;
119+
96120
#endregion
97121

98122
#region Private Properties
@@ -445,59 +469,19 @@ public bool MouseMove(int previousMouseX, int previousMouseY, int mouseX, int mo
445469
_rubberbandPrimitive.Vertices.Add(_rubberbandStartInWorld.X);
446470
_rubberbandPrimitive.Vertices.Add(endPoint.Y);
447471
_rubberbandPrimitive.Vertices.Add(0.0f);
448-
449-
// Declare some screen coordinates
450-
float pt1X;
451-
float pt1Y;
452-
float pt2X;
453-
float pt2Y;
454-
472+
455473
// To simplify the logic always assuming the rubberband is
456-
// moving from left to right, top to bottom.
457-
// The following logic keeps the points in this order.
458-
459-
// If the rubberband start X position is less than the current mouse X position then...
460-
if (_rubberBandStartX < mouseX)
461-
{
462-
// If the rubberband start Y position is less than the current mouse Y position then...
463-
if (_rubberBandStartY < mouseY)
464-
{
465-
pt1X = _rubberBandStartX;
466-
pt1Y = _rubberBandStartY;
467-
pt2X = mouseX;
468-
pt2Y = mouseY;
469-
}
470-
else
471-
{
472-
pt1X = _rubberBandStartX;
473-
pt1Y = mouseY;
474-
pt2X = mouseX;
475-
pt2Y = _rubberBandStartY;
476-
}
477-
}
478-
else
479-
{
480-
if (_rubberBandStartY < mouseY)
481-
{
482-
pt1X = mouseX;
483-
pt1Y = _rubberBandStartY;
484-
pt2X = _rubberBandStartX;
485-
pt2Y = mouseY;
486-
}
487-
else
488-
{
489-
pt1X = mouseX;
490-
pt1Y = mouseY;
491-
pt2X = _rubberBandStartX;
492-
pt2Y = _rubberBandStartY;
493-
}
494-
}
495-
474+
// moving from left to right, top to bottom.
475+
_normalizedRubberbandPt1X = Math.Min(_rubberBandStartX, mouseX);
476+
_normalizedRubberbandPt2X = Math.Max(_rubberBandStartX, mouseX);
477+
_normalizedRubberbandPt1Y = Math.Min(_rubberBandStartY, mouseY);
478+
_normalizedRubberbandPt2Y = Math.Max(_rubberBandStartY, mouseY);
479+
496480
// Convert the screen coordinates into 3-D world coordinates
497-
Vector3 leftTop = GetMousePointInWorld(new Vector2(pt1X, pt1Y));
498-
Vector3 rightTop = GetMousePointInWorld(new Vector2(pt2X, pt1Y));
499-
Vector3 bottomRight = GetMousePointInWorld(new Vector2(pt2X, pt2Y));
500-
Vector3 bottomLeft = GetMousePointInWorld(new Vector2(pt1X, pt2Y));
481+
Vector3 leftTop = GetMousePointInWorld(new Vector2(_normalizedRubberbandPt1X, _normalizedRubberbandPt1Y));
482+
Vector3 rightTop = GetMousePointInWorld(new Vector2(_normalizedRubberbandPt2X, _normalizedRubberbandPt1Y));
483+
Vector3 bottomRight = GetMousePointInWorld(new Vector2(_normalizedRubberbandPt2X, _normalizedRubberbandPt2Y));
484+
Vector3 bottomLeft = GetMousePointInWorld(new Vector2(_normalizedRubberbandPt1X, _normalizedRubberbandPt2Y));
501485

502486
// Loop over all the props
503487
foreach (IPropOpenGLData prop in Props)
@@ -528,7 +512,7 @@ public bool MouseMove(int previousMouseX, int previousMouseY, int mouseX, int mo
528512
{
529513
// If the prop intersects the rubberband selection or
530514
// contains the entire prop then...
531-
if (IntersectsProp(leftTop, rightTop, bottomRight, bottomLeft, prop.GetMinimum(), prop.GetMaximum()) ||
515+
if (IntersectsProp(prop.GetMinimum(), prop.GetMaximum()) ||
532516
ContainsProp(leftTop, bottomRight, prop.GetMinimum(), prop.GetMaximum()))
533517
{
534518
// If the prop is not already selected then...
@@ -636,25 +620,18 @@ public bool MouseMove(int previousMouseX, int previousMouseY, int mouseX, int mo
636620

637621
/// <summary>
638622
/// Returns true if the prop represented by the minimum and maximum vectors intersect with
639-
/// the rectangle formed by topLeft, topRight, bottomRight, and bottomLeft.
640-
/// </summary>
641-
/// <param name="topLeft">Top left point of the rectangle</param>
642-
/// <param name="bottomRight">Bottom right point of the rectangle</param>
623+
/// the rubberband frustum.
624+
/// </summary>
643625
/// <param name="minimum">Minimum values of the prop along each axis</param>
644626
/// <param name="maximum">Maximum values of the prop along each axis</param>
645-
/// <returns>True if the prop intersects with the rectangle</returns>
646-
private bool IntersectsProp(Vector3 topLeft, Vector3 topRight, Vector3 bottomRight, Vector3 bottomLeft, Vector3 minimum, Vector3 maximum)
627+
/// <returns>True if the prop intersects with the frustum created from the rubberband rectangle</returns>
628+
private bool IntersectsProp(
629+
Vector3 minimum,
630+
Vector3 maximum)
647631
{
648-
// Return whether the prop intersects with the rectangle
649-
return
650-
((topLeft.X >= minimum.X && topLeft.X <= maximum.X &&
651-
topLeft.Y >= minimum.Y && topLeft.Y <= maximum.Y) ||
652-
(bottomRight.X >= minimum.X && bottomRight.X <= maximum.X &&
653-
bottomRight.Y >= minimum.Y && bottomRight.Y <= maximum.Y) ||
654-
(topRight.X >= minimum.X && topRight.X <= maximum.X &&
655-
topRight.Y >= minimum.Y && topRight.Y <= maximum.Y) ||
656-
(bottomLeft.X >= minimum.X && bottomLeft.X <= maximum.X &&
657-
bottomLeft.Y >= minimum.Y && bottomLeft.Y <= maximum.Y));
632+
// Check to see if the prop intersects with the rubberband frustum
633+
return IsBoxIntersectingFrustum(
634+
CreateSelectionFrustum(), minimum, maximum);
658635
}
659636

660637
/// <summary>
@@ -1277,8 +1254,8 @@ private Vector3 GetMouseMovementInWorld(int previousMouseX, int previousMouseY,
12771254

12781255
float depth = (clip.Z + 1.0f) * 0.5f; // convert NDC Z → depth buffer range
12791256

1280-
var lastWorld = Unproject(new Vector3(previousMouseX, previousMouseY, depth), Camera.ViewMatrix, CreatePerspective(), new Size(OpenTkControl_Width, OpenTkControl_Height));
1281-
var currWorld = Unproject(new Vector3(mouseX, mouseY, depth), Camera.ViewMatrix, CreatePerspective(), new Size(OpenTkControl_Width, OpenTkControl_Height));
1257+
Vector3 lastWorld = Unproject(previousMouseX, previousMouseY, depth, Camera.ViewMatrix, CreatePerspective());
1258+
Vector3 currWorld = Unproject(mouseX, mouseY, depth, Camera.ViewMatrix, CreatePerspective());
12821259

12831260
propPos = currWorld - lastWorld;
12841261

@@ -1288,34 +1265,105 @@ private Vector3 GetMouseMovementInWorld(int previousMouseX, int previousMouseY,
12881265
/// <summary>
12891266
/// Unprojects a screen coordinate into world coordinates.
12901267
/// </summary>
1291-
/// <param name="screenPos">Screen position to unproject</param>
1268+
/// <param name="screenX">Screen position on the X axis</param>
1269+
/// <param name="screenY">Screen position on the Y axis</param>
1270+
/// <param name="depth">
1271+
/// The depth value in the range [0.0, 1.0], where 0.0 represents
1272+
/// the Near clipping plane and 1.0 represents the Far clipping plane.
1273+
/// </param>
12921274
/// <param name="view">View matrix</param>
12931275
/// <param name="proj">Project matrix</param>
1294-
/// <param name="viewport">Size of the viewport</param>
12951276
/// <returns>Screen position in world coordinates</returns>
1296-
private Vector3 Unproject(Vector3 screenPos, Matrix4 view, Matrix4 proj, Size viewport)
1277+
private Vector3 Unproject(float screenX, float screenY, float depth, Matrix4 view, Matrix4 proj)
12971278
{
1298-
Vector4 vec;
1279+
// 1. Map Screen Pixels to NDC (-1 to 1)
1280+
float ndcX = (2.0f * screenX / OpenTkControl_Width) - 1.0f;
1281+
float ndcY = 1.0f - (2.0f * screenY / OpenTkControl_Height);
12991282

1300-
vec.X = 2.0f * screenPos.X / viewport.Width - 1.0f;
1301-
vec.Y = 1.0f - 2.0f * screenPos.Y / viewport.Height;
1302-
vec.Z = screenPos.Z * 2.0f - 1.0f;
1303-
vec.W = 1.0f;
1283+
// 2. Map Depth (0 to 1) to NDC (-1 to 1)
1284+
// This is the missing piece in your second method!
1285+
float ndcZ = depth; //(depth * 2.0f) - 1.0f;
13041286

1305-
Matrix4 inv = Matrix4.Invert(view * proj);
1306-
// Vector4 result = OpenTK.Mathematics.Vector4.Transform(vec, inv);
1307-
Vector4 result = new Vector4(
1308-
vec.X * inv.M11 + vec.Y * inv.M21 + vec.Z * inv.M31 + vec.W * inv.M41,
1309-
vec.X * inv.M12 + vec.Y * inv.M22 + vec.Z * inv.M32 + vec.W * inv.M42,
1310-
vec.X * inv.M13 + vec.Y * inv.M23 + vec.Z * inv.M33 + vec.W * inv.M43,
1311-
vec.X * inv.M14 + vec.Y * inv.M24 + vec.Z * inv.M34 + vec.W * inv.M44);
1287+
// 3. Transform by Inverse View-Projection
1288+
Matrix4 invVP = Matrix4.Invert(view * proj);
1289+
Vector4 ndc = new Vector4(ndcX, ndcY, ndcZ, 1.0f);
1290+
1291+
// Using TransformRow to match OpenTK's Matrix * Vector convention
1292+
Vector4 world = Vector4.TransformRow(ndc, invVP);
1293+
1294+
// 4. Perspective Divide (Crucial for 3D depth)
1295+
if (Math.Abs(world.W) > float.Epsilon)
1296+
{
1297+
return world.Xyz / world.W;
1298+
}
1299+
1300+
return world.Xyz;
1301+
}
13121302

1313-
if (result.W > float.Epsilon)
1314-
result /= result.W;
1303+
/// <summary>
1304+
/// Creates the rubberband selection frustum.
1305+
/// </summary>
1306+
/// <returns>Selection frustum as a collection of planes</returns>
1307+
private List<Plane> CreateSelectionFrustum()
1308+
{
1309+
// Top-Left corner on the Near plane
1310+
Vector3 ptTL_Near = Unproject(_normalizedRubberbandPt1X, _normalizedRubberbandPt1Y, 0.0f, Camera.ViewMatrix, CreatePerspective());
1311+
// Bottom-Right corner on the Near plane
1312+
Vector3 ptBR_Near = Unproject(_normalizedRubberbandPt2X, _normalizedRubberbandPt2Y, 0.0f, Camera.ViewMatrix, CreatePerspective());
1313+
Vector3 ptTR_Near = Unproject(_normalizedRubberbandPt2X, _normalizedRubberbandPt1Y, 0.0f, Camera.ViewMatrix, CreatePerspective());
1314+
Vector3 ptBL_Near = Unproject(_normalizedRubberbandPt1X, _normalizedRubberbandPt2Y, 0.0f, Camera.ViewMatrix, CreatePerspective());
1315+
1316+
// Top-Left corner on the Far plane
1317+
Vector3 ptTL_Far = Unproject(_normalizedRubberbandPt1X, _normalizedRubberbandPt1Y, 1.0f, Camera.ViewMatrix, CreatePerspective());
1318+
// Bottom-Right corner on the Far plane
1319+
Vector3 ptBR_Far = Unproject(_normalizedRubberbandPt2X, _normalizedRubberbandPt2Y, 1.0f, Camera.ViewMatrix, CreatePerspective());
1320+
Vector3 ptTR_Far = Unproject(_normalizedRubberbandPt2X, _normalizedRubberbandPt1Y, 1.0f, Camera.ViewMatrix, CreatePerspective());
1321+
Vector3 ptBL_Far = Unproject(_normalizedRubberbandPt1X, _normalizedRubberbandPt2Y, 1.0f, Camera.ViewMatrix, CreatePerspective());
1322+
1323+
List<Plane> selectionFrustum = new List<Plane>
1324+
{
1325+
new Plane(ptTL_Near, ptBL_Near, ptTR_Near), // Near Plane
1326+
new Plane(ptTL_Far, ptTR_Far, ptBL_Far), // Far Plane
1327+
new Plane(ptBL_Near, ptTL_Near, ptBL_Far), // Left Plane
1328+
new Plane(ptTR_Near, ptBR_Near, ptTR_Far), // Right Plane
1329+
new Plane(ptTL_Near, ptTR_Near, ptTL_Far), // Top Plane
1330+
new Plane(ptBL_Near, ptBL_Far, ptBR_Near) // Bottom Plane
1331+
};
1332+
1333+
return selectionFrustum;
1334+
}
13151335

1316-
return new Vector3(result.X, result.Y, result.Z);
1336+
/// <summary>
1337+
/// Returns true if the specified box intersects the frustum.
1338+
/// </summary>
1339+
/// <param name="frustum">Frustum to test</param>
1340+
/// <param name="boxMin">Box minimum point</param>
1341+
/// <param name="boxMax">Box maximum point</param>
1342+
/// <returns>True if the box intersects the frustum</returns>
1343+
private bool IsBoxIntersectingFrustum(List<Plane> frustum, Vector3 boxMin, Vector3 boxMax)
1344+
{
1345+
// Loop over the planes in the frustum
1346+
foreach (Plane plane in frustum)
1347+
{
1348+
// The "Negative Vertex" is the corner of the box furthest OPPOSITE the normal
1349+
Vector3 negativeVertex = new Vector3(
1350+
plane.Normal.X >= 0 ? boxMin.X : boxMax.X,
1351+
plane.Normal.Y >= 0 ? boxMin.Y : boxMax.Y,
1352+
plane.Normal.Z >= 0 ? boxMin.Z : boxMax.Z
1353+
);
1354+
1355+
// If the point furthest along the normal is behind the plane,
1356+
// the entire box is definitely outside this plane.
1357+
if (Vector3.Dot(plane.Normal, negativeVertex) > plane.Distance)
1358+
{
1359+
// Note: If you find ONE plane where the box is entirely on the "outside"
1360+
// side, there is no intersection.
1361+
return false;
1362+
}
1363+
}
1364+
return true;
13171365
}
13181366

13191367
#endregion
1320-
}
1368+
}
13211369
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using OpenTK.Mathematics;
2+
3+
namespace VixenApplication.SetupDisplay.OpenGL
4+
{
5+
/// <summary>
6+
/// Maintains a plane in 3-D space.
7+
/// </summary>
8+
/// <remarks>
9+
/// This class is used to support creating a frustrum.
10+
/// </remarks>
11+
public class Plane
12+
{
13+
#region Constructor
14+
15+
/// <summary>
16+
/// Constructor
17+
/// </summary>
18+
/// <param name="p1">First point in plane</param>
19+
/// <param name="p2">Second point in plane</param>
20+
/// <param name="p3">Third point in plane</param>
21+
public Plane(Vector3 p1, Vector3 p2, Vector3 p3)
22+
{
23+
Vector3 v1 = p2 - p1;
24+
Vector3 v2 = p3 - p1;
25+
Normal = Vector3.Normalize(Vector3.Cross(v1, v2));
26+
Distance = Vector3.Dot(Normal, p1);
27+
}
28+
29+
#endregion
30+
31+
#region Public Properties
32+
33+
/// <summary>
34+
/// Defines the normal vector for the plane.
35+
/// </summary>
36+
public Vector3 Normal { get; set; }
37+
38+
/// <summary>
39+
/// Maintains the signed distance from the world origin (0, 0, 0) to the plane,
40+
/// measured along the plane's normal vector.
41+
/// </summary>
42+
public float Distance { get; set; }
43+
44+
#endregion
45+
46+
#region Public Methods
47+
48+
/// <summary>
49+
/// Returns true if the specified point is in front of the plane.
50+
/// In front is determined by the plane normal vector.
51+
/// </summary>
52+
/// <param name="point">Point to test</param>
53+
/// <returns>True if the point is in front of the plane.</returns>
54+
public bool IsPointInFront(Vector3 point)
55+
{
56+
return Vector3.Dot(Normal, point) >= Distance;
57+
}
58+
59+
#endregion
60+
}
61+
}

src/Vixen.Application/Vixen.Application.csproj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
<ItemGroup>
2020
<None Remove="NLog.config" />
2121
<None Remove="SetupDisplay\Views\BackgroundImage.png" />
22+
<None Remove="SetupDisplay\Views\buttonContainsSelect.png" />
2223
<None Remove="SetupDisplay\Views\buttonPartialSelect.png" />
2324
<None Remove="SetupDisplay\Views\buttonSelect.png" />
25+
<None Remove="SetupDisplay\Views\hand-drag.png" />
2426
</ItemGroup>
2527

2628
<ItemGroup>
@@ -31,10 +33,13 @@
3133
</ItemGroup>
3234

3335
<ItemGroup>
34-
<Resource Include="SetupDisplay\Views\buttonPartialSelect.png">
36+
<Resource Include="SetupDisplay\Views\buttonContainsSelect.png">
3537
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
3638
</Resource>
37-
<Resource Include="SetupDisplay\Views\buttonSelect.png">
39+
</ItemGroup>
40+
41+
<ItemGroup>
42+
<Resource Include="SetupDisplay\Views\buttonPartialSelect.png">
3843
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
3944
</Resource>
4045
</ItemGroup>
@@ -114,6 +119,9 @@
114119
<Resource Include="SetupDisplay\Views\BackgroundImage.png">
115120
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
116121
</Resource>
122+
<Resource Include="SetupDisplay\Views\buttonSelect.png">
123+
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
124+
</Resource>
117125
</ItemGroup>
118126

119127
</Project>

0 commit comments

Comments
 (0)