From 6d507407c28604051caa896192d46bc5caefafbe Mon Sep 17 00:00:00 2001 From: Craig Long Date: Wed, 13 Dec 2023 17:20:31 -0500 Subject: [PATCH] DYN-6489 Dynamo Zoom Extents behavior has changed (#14674) * Adopt updated helix ZoomExtents algorithm * use Helix native bounds implementation * Why was this ever this way * changes made * handle zoom for ortho * review comments --------- Co-authored-by: Craig Long Co-authored-by: mjk.kirschner --- .../Watch3D/HelixWatch3DViewModel.cs | 20 +-- .../Views/Core/DynamoView.xaml.cs | 1 - .../Views/Preview/Watch3DView.xaml.cs | 160 +++++++++++++++++- 3 files changed, 164 insertions(+), 17 deletions(-) diff --git a/src/DynamoCoreWpf/ViewModels/Watch3D/HelixWatch3DViewModel.cs b/src/DynamoCoreWpf/ViewModels/Watch3D/HelixWatch3DViewModel.cs index 9e0cbb6c5d8..0f45cdcc675 100644 --- a/src/DynamoCoreWpf/ViewModels/Watch3D/HelixWatch3DViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Watch3D/HelixWatch3DViewModel.cs @@ -3006,27 +3006,25 @@ internal static Rect3D ToRect3D(this BoundingBox bounds, double minRectSize = 0. /// A object encapsulating the geometry. internal static BoundingBox Bounds(this GeometryModel3D geom, float defaultBoundsSize = 5.0f) { - if (geom.Geometry.Positions == null || geom.Geometry.Positions.Count == 0) - { - return new BoundingBox(); - } + var bounds = geom.Bounds; - if (geom.Geometry.Positions.Count > 1) + //if the actual bounds diagonal are smaller than the default bounds diagonal then return + //a new default bounds centered on the actual bounds center. + if(bounds.Size.LengthSquared() < defaultBoundsSize * defaultBoundsSize * 3) { - return BoundingBox.FromPoints(geom.Geometry.Positions.ToArray()); + var pos = bounds.Center(); + var min = pos + new Vector3(-defaultBoundsSize, -defaultBoundsSize, -defaultBoundsSize); + var max = pos + new Vector3(defaultBoundsSize, defaultBoundsSize, defaultBoundsSize); + return new BoundingBox(min, max); } - var pos = geom.Geometry.Positions.First(); - var min = pos + new Vector3(-defaultBoundsSize, -defaultBoundsSize, -defaultBoundsSize); - var max = pos + new Vector3(defaultBoundsSize, defaultBoundsSize, defaultBoundsSize); - return new BoundingBox(min, max); + return bounds; } public static Vector3 Center(this BoundingBox bounds) { return (bounds.Maximum + bounds.Minimum)/2; } - } internal static class Vector3Extensions diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs index 00e7289c873..91b9a2263ac 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs @@ -1360,7 +1360,6 @@ private void DynamoView_Loaded(object sender, EventArgs e) BackgroundPreview = new Watch3DView { Name = BackgroundPreviewName }; background_grid.Children.Add(BackgroundPreview); BackgroundPreview.DataContext = dynamoViewModel.BackgroundPreviewViewModel; - BackgroundPreview.Margin = new System.Windows.Thickness(0, 20, 0, 0); var vizBinding = new Binding { Source = dynamoViewModel.BackgroundPreviewViewModel, diff --git a/src/DynamoCoreWpf/Views/Preview/Watch3DView.xaml.cs b/src/DynamoCoreWpf/Views/Preview/Watch3DView.xaml.cs index 296d601e9bc..e0fb71e17e9 100644 --- a/src/DynamoCoreWpf/Views/Preview/Watch3DView.xaml.cs +++ b/src/DynamoCoreWpf/Views/Preview/Watch3DView.xaml.cs @@ -11,7 +11,9 @@ using HelixToolkit.Wpf.SharpDX; using HelixToolkit.SharpDX.Core; using SharpDX; +using OrthographicCamera = HelixToolkit.Wpf.SharpDX.OrthographicCamera; using Point = System.Windows.Point; +using HelixToolkit.SharpDX.Core.Cameras; namespace Dynamo.Controls { @@ -172,18 +174,166 @@ private void ViewLoadedHandler(object sender, RoutedEventArgs e) private void ViewModel_RequestZoomToFit(BoundingBox bounds) { - var prevcamDir = watch_view.Camera.LookDirection; - watch_view.ZoomExtents(bounds.ToRect3D(.05)); + var preCamDir = watch_view.Camera.LookDirection; + //TODO, Call the equivalent method in Helix on adoption of next release, remove these private helix definitions. + + if (watch_view.Camera is HelixToolkit.Wpf.SharpDX.PerspectiveCamera perspectiveCam) + { + ZoomExtents(perspectiveCam, (float)(watch_view.ActualWidth / watch_view.ActualHeight), bounds, out var pos, + out var look, out var up); + perspectiveCam.AnimateTo(pos.ToPoint3D(), look.ToVector3D(), up.ToVector3D(), 0); + } + + else if (watch_view.Camera is OrthographicCamera orthCam && watch_view.Camera.CameraInternal is OrthographicCameraCore orthoCamCore) + { + ZoomExtents(orthoCamCore, (float)(watch_view.ActualWidth / watch_view.ActualHeight), bounds, out var pos, + out var look, out var up,out var width + ); + orthCam.AnimateWidth(width,0); + orthCam.AnimateTo(pos.ToPoint3D(), look.ToVector3D(), up.ToVector3D(), 0); + } + //if after a zoom the camera is in an undefined position or view direction, reset it. - if(watch_view.Camera.Position.ToVector3().IsUndefined() || - watch_view.Camera.LookDirection.ToVector3().IsUndefined() || + if (watch_view.Camera.Position.ToVector3().IsUndefined() || + watch_view.Camera.LookDirection.ToVector3().IsUndefined() || watch_view.Camera.LookDirection.Length == 0) { watch_view.Camera.Position = prevCamera; - watch_view.Camera.LookDirection = prevcamDir; + watch_view.Camera.LookDirection = preCamDir; + } + } + + #region ZoomExtents + + //This implementation is found in the current dev branch of Helix-Toolkit (https://github.com/helix-toolkit/helix-toolkit/tree/develop) with the assumption it will be included in the 2.25.0 release + //The PR including these changes "Re-implementing zoom extents in sharpdx versions" is found here -> https://github.com/helix-toolkit/helix-toolkit/pull/2003 + //Specifically the code included here is located in https://github.com/holance/helix-toolkit/blob/develop/Source/HelixToolkit.SharpDX.Shared/Extensions/CameraCoreExtensions.cs in this commit: + //https://github.com/holance/helix-toolkit/commit/660c85ff2218eb318f810dd682986d1816c5ece5 + //This implementation is adjusted to remove the instance method pattern. + //This section can be removed when we adopt the next release of helix-toolkit and call the ZoomExtents() directly. + + private static void ZoomExtents(HelixToolkit.Wpf.SharpDX.PerspectiveCamera camera, float aspectRatio, BoundingBox boundingBox, out Vector3 position, out Vector3 lookDir, out Vector3 upDir) + { + var cameraDir = Vector3.Normalize(camera.LookDirection.ToVector3()); + var cameraUp = Vector3.Normalize(camera.UpDirection.ToVector3()); + var cameraRight = Vector3.Cross(cameraDir, cameraUp); + cameraUp = Vector3.Cross(cameraRight, cameraDir); + + var corners = boundingBox.GetCorners(); + + var frustum = new BoundingFrustum(camera.CreateViewMatrix() * camera.CreateProjectionMatrix(aspectRatio)); + var leftNormal = -frustum.Left.Normal; + var rightNormal = -frustum.Right.Normal; + var topNormal = -frustum.Top.Normal; + var bottomNormal = -frustum.Bottom.Normal; + + int leftMostPoint = -1, rightMostPoint = -1, topMostPoint = -1, bottomMostPoint = -1; + for (int i = 0; i < corners.Length; i++) + { + if (leftMostPoint < 0 && IsOutermostPointInDirection(i, ref leftNormal, corners)) + { + leftMostPoint = i; + } + if (rightMostPoint < 0 && IsOutermostPointInDirection(i, ref rightNormal, corners)) + { + rightMostPoint = i; + } + if (topMostPoint < 0 && IsOutermostPointInDirection(i, ref topNormal, corners)) + { + topMostPoint = i; + } + if (bottomMostPoint < 0 && IsOutermostPointInDirection(i, ref bottomNormal, corners)) + { + bottomMostPoint = i; + } + } + + var plane1 = new Plane(corners[leftMostPoint], leftNormal); + var plane2 = new Plane(corners[rightMostPoint], rightNormal); + PlaneIntersectsPlane(ref plane1, ref plane2, out var horizontalIntersection); + plane1 = new Plane(corners[topMostPoint], topNormal); + plane2 = new Plane(corners[bottomMostPoint], bottomNormal); + PlaneIntersectsPlane(ref plane1, ref plane2, out var verticalIntersection); + FindClosestPointsOnTwoLines(ref horizontalIntersection, ref verticalIntersection, out var closestPointLine1, out var closestPointLine2); + position = Vector3.Dot(closestPointLine1 - closestPointLine2, cameraDir) < 0 ? closestPointLine1 : closestPointLine2; + upDir = cameraUp; + var boundPlane = new Plane(boundingBox.Center, cameraDir); + var lookRay = new Ray(position, cameraDir); + boundPlane.Intersects(ref lookRay, out float dist); + lookDir = cameraDir * dist; + } + + private static void ZoomExtents(OrthographicCameraCore camera, float aspectRatio, BoundingBox boundingBox, out Vector3 position, out Vector3 lookDir, out Vector3 upDir, out float width) + { + float minX = float.PositiveInfinity, minY = float.PositiveInfinity, maxX = float.NegativeInfinity, maxY = float.NegativeInfinity; + var corners = boundingBox.GetCorners(); + var view = camera.CreateViewMatrix(); + foreach (var p in corners) + { + var local = Vector3.TransformCoordinate(p, view); + minX = Math.Min(minX, local.X); + minY = Math.Min(minY, local.Y); + maxX = Math.Max(maxX, local.X); + maxY = Math.Max(maxY, local.Y); + } + width = aspectRatio > 1 ? Math.Max((maxX - minX), (maxY - minY) * aspectRatio) : Math.Max((maxX - minX) / aspectRatio, maxY - minY); + position = boundingBox.Center - camera.LookDirection.Normalized() * width; + lookDir = camera.LookDirection.Normalized() * width; + upDir = camera.UpDirection; + } + + private static bool IsOutermostPointInDirection(int pointIndex, ref Vector3 direction, Vector3[] corners) + { + Vector3 point = corners[pointIndex]; + for (int i = 0; i < corners.Length; i++) + { + if (i != pointIndex && Vector3.Dot(direction, corners[i] - point) > 0) + return false; + } + + return true; + } + + // Credit: http://wiki.unity3d.com/index.php/3d_Math_functions + // Returns the edge points of the closest line segment between 2 lines + private static void FindClosestPointsOnTwoLines(ref Ray line1, ref Ray line2, out Vector3 closestPointLine1, out Vector3 closestPointLine2) + { + Vector3 line1Direction = line1.Direction; + Vector3 line2Direction = line2.Direction; + + float a = Vector3.Dot(line1Direction, line1Direction); + float b = Vector3.Dot(line1Direction, line2Direction); + float e = Vector3.Dot(line2Direction, line2Direction); + + float d = a * e - b * b; + + Vector3 r = line1.Position - line2.Position; + float c = Vector3.Dot(line1Direction, r); + float f = Vector3.Dot(line2Direction, r); + + float s = (b * f - c * e) / d; + float t = (a * f - c * b) / d; + + closestPointLine1 = line1.Position + line1Direction * s; + closestPointLine2 = line2.Position + line2Direction * t; + } + + private static bool PlaneIntersectsPlane(ref Plane p1, ref Plane p2, out Ray intersection) + { + var dir = Vector3.Cross(p1.Normal, p2.Normal); + float det = Vector3.Dot(dir, dir); + if (Math.Abs(det) > float.Epsilon) + { + var p = (Vector3.Cross(dir, p2.Normal) * p1.D + Vector3.Cross(p1.Normal, dir) * p2.D) / det; + intersection = new Ray(p, dir); + return true; } + intersection = default; + return false; } + #endregion + private void RequestViewRefreshHandler() { View.InvalidateRender();