From 8fafc54f900c112db2f191f8d4b4ce8b3dbd779d Mon Sep 17 00:00:00 2001 From: fortsnek9348 Date: Wed, 2 Mar 2022 00:52:04 +0000 Subject: [PATCH] Orthographic projection mode with dynamic near/far. --- .../GCodeRenderer/GCodeRenderer.cs | 36 +- .../GCodeRenderer/GCodeVertexBuffer.cs | 12 + .../ApplicationView/Config/BedConfig.cs | 17 + .../EditorTools/RotateControls/PathControl.cs | 9 + .../RotateControls/RotateCornerControl.cs | 59 +- .../ScaleControls/ScaleDiameterControl.cs | 55 +- .../ScaleControls/ScaleHeightControl.cs | 34 +- .../ScaleControls/ScaleMatrixCornerControl.cs | 44 +- .../ScaleControls/ScaleMatrixEdgeControl.cs | 28 +- .../ScaleControls/ScaleMatrixTopControl.cs | 37 +- .../ScaleWidthDepthCornerControl.cs | 43 +- .../ScaleWidthDepthEdgeControl.cs | 36 +- .../DesignTools/Interfaces/IEditorDraw.cs | 4 + .../DesignTools/Obsolete/CurveObject3D.cs | 13 + .../Obsolete/FitToBoundsObject3D.cs | 63 +- .../Operations/ArrayRadialObject3D.cs | 5 + .../DesignTools/Operations/CurveObject3D_2.cs | 35 +- .../DesignTools/Operations/CurveObject3D_3.cs | 41 +- .../Operations/FitToBoundsObject3D_2.cs | 17 +- .../Operations/FitToBoundsObject3D_3.cs | 16 +- .../Operations/FitToCylinderObject3D.cs | 6 + .../Operations/Image/ImageToPathObject3D.cs | 5 + .../Operations/Image/ImageToPathObject3D_2.cs | 5 + .../Operations/Image/PathObject3D.cs | 6 + .../Operations/Object3DExtensions.cs | 34 + .../Operations/Path/InflatePathObject3D.cs | 6 + .../Operations/Path/MergePathObject3D.cs | 5 + .../Operations/Path/OutlinePathObject3D.cs | 6 + .../Operations/Path/RevolveObject3D.cs | 35 +- .../Operations/Path/SmoothPathObject3D.cs | 6 + .../Operations/RotateObject3D_2.cs | 12 + .../DesignTools/Operations/ScaleObject3D.cs | 12 + .../DesignTools/Operations/TwistObject3D.cs | 8 + .../DesignTools/Primitives/BaseObject3D.cs | 20 +- .../DesignTools/Primitives/BoxPathObject3D.cs | 5 + .../Primitives/DescriptionObject3D.cs | 11 +- .../Primitives/MeasureToolObject3D.cs | 5 + .../Primitives/SetTemperatureObject3D.cs | 5 + .../Primitives/TextPathObject3D.cs | 5 + .../TracedPositionObject3DControl.cs | 5 + .../SceneViewer/AABBDrawable.cs | 6 + .../SceneViewer/AxisIndicatorDrawable.cs | 12 + .../SceneViewer/FloorDrawable.cs | 32 +- .../SceneViewer/FrustumDrawable.cs | 104 +++ .../SceneViewer/IDrawable.cs | 3 + .../SceneViewer/IDrawableItem.cs | 3 + .../SceneViewer/InspectedItemDrawable.cs | 17 + .../SceneViewer/ItemTraceDataDrawable.cs | 10 + .../SceneViewer/NormalsDrawable.cs | 12 + .../Object3DControlBoundingBoxesDrawable.cs | 75 ++ .../SceneViewer/SceneTraceDataDrawable.cs | 6 + .../SceneViewer/SelectedItemDrawable.cs | 43 +- .../Actions/SubtractAndReplaceObject3D_2.cs | 5 + .../View3D/Actions/SubtractObject3D_2.cs | 5 + .../View3D/Actions/SubtractPathObject3D.cs | 5 + .../View3D/BedMeshGenerator.cs | 4 + .../View3D/CameraFittingUtil.cs | 758 ++++++++++++++++++ .../View3D/Gui3D/MoveInZControl.cs | 29 +- .../View3D/Gui3D/SelectionShadow.cs | 14 + .../View3D/Gui3D/SnappingIndicators.cs | 6 + .../View3D/Interaction/IObject3DControl.cs | 4 + .../View3D/Interaction/Object3DControl.cs | 3 + .../View3D/LevelingDataDrawable.cs | 10 + .../View3D/Object3DControlsLayer.cs | 76 +- .../View3D/TrackballTumbleWidgetExtended.cs | 322 +++++++- .../PartPreviewWindow/View3D/View3DWidget.cs | 221 +++-- .../View3D/WorldViewExtensions.cs | 6 + Submodules/agg-sharp | 2 +- .../CameraFittingUtilTests.cs | 192 +++++ .../MatterControl.AutomationTests.csproj | 1 + 70 files changed, 2548 insertions(+), 244 deletions(-) create mode 100644 MatterControlLib/PartPreviewWindow/SceneViewer/FrustumDrawable.cs create mode 100644 MatterControlLib/PartPreviewWindow/SceneViewer/Object3DControlBoundingBoxesDrawable.cs create mode 100644 MatterControlLib/PartPreviewWindow/View3D/CameraFittingUtil.cs create mode 100644 Tests/MatterControl.AutomationTests/CameraFittingUtilTests.cs diff --git a/MatterControl.OpenGL/GCodeRenderer/GCodeRenderer.cs b/MatterControl.OpenGL/GCodeRenderer/GCodeRenderer.cs index 3dd1f31e8..88647b2e1 100644 --- a/MatterControl.OpenGL/GCodeRenderer/GCodeRenderer.cs +++ b/MatterControl.OpenGL/GCodeRenderer/GCodeRenderer.cs @@ -34,6 +34,7 @@ using MatterHackers.Agg; using MatterHackers.Agg.UI; using MatterHackers.RenderOpenGl; using MatterHackers.RenderOpenGl.OpenGl; +using MatterHackers.VectorMath; namespace MatterHackers.GCodeVisualizer { @@ -330,12 +331,11 @@ namespace MatterHackers.GCodeVisualizer } } } - - public void Render3D(GCodeRenderInfo renderInfo, DrawEventArgs e) + bool PrepareForGeometryGeneration(GCodeRenderInfo renderInfo) { if (renderInfo == null) { - return; + return false; } if (layerVertexBuffer == null) @@ -359,7 +359,12 @@ namespace MatterHackers.GCodeVisualizer lastRenderType = renderInfo.CurrentRenderType; } - if (all.layers.Count > 0) + return all.layers.Count > 0; + } + + public void Render3D(GCodeRenderInfo renderInfo, DrawEventArgs e) + { + if (PrepareForGeometryGeneration(renderInfo)) { for (int i = renderInfo.EndLayerIndex - 1; i >= renderInfo.StartLayerIndex; i--) { @@ -425,5 +430,28 @@ namespace MatterHackers.GCodeVisualizer GL.PopAttrib(); } } + + public AxisAlignedBoundingBox GetAabbOfRender3D(GCodeRenderInfo renderInfo) + { + var box = AxisAlignedBoundingBox.Empty(); + + if (PrepareForGeometryGeneration(renderInfo)) + { + for (int i = renderInfo.EndLayerIndex - 1; i >= renderInfo.StartLayerIndex; i--) + { + if (i < layerVertexBuffer.Count) + { + if (layerVertexBuffer[i] == null) + { + layerVertexBuffer[i] = Create3DDataForLayer(i, renderInfo); + } + + box = AxisAlignedBoundingBox.Union(box, layerVertexBuffer[i].BoundingBox); + } + } + } + + return box; + } } } \ No newline at end of file diff --git a/MatterControl.OpenGL/GCodeRenderer/GCodeVertexBuffer.cs b/MatterControl.OpenGL/GCodeRenderer/GCodeVertexBuffer.cs index 8fd781279..ddaf61c00 100644 --- a/MatterControl.OpenGL/GCodeRenderer/GCodeVertexBuffer.cs +++ b/MatterControl.OpenGL/GCodeRenderer/GCodeVertexBuffer.cs @@ -33,6 +33,7 @@ using MatterHackers.Agg; using MatterHackers.Agg.UI; using MatterHackers.RenderOpenGl; using MatterHackers.RenderOpenGl.OpenGl; +using MatterHackers.VectorMath; namespace MatterHackers.GCodeVisualizer { @@ -48,6 +49,10 @@ namespace MatterHackers.GCodeVisualizer private ColorVertexData[] colorVertexData; + private AxisAlignedBoundingBox boundingBox = AxisAlignedBoundingBox.Empty(); + + public AxisAlignedBoundingBox BoundingBox { get { return new AxisAlignedBoundingBox(boundingBox.MinXYZ, boundingBox.MaxXYZ); } } + /// /// Create a new VertexBuffer /// @@ -136,6 +141,13 @@ namespace MatterHackers.GCodeVisualizer } } } + + boundingBox = AxisAlignedBoundingBox.Empty(); + foreach (int i in indexData) + { + var v = colorData[i]; + boundingBox.ExpandToInclude(new Vector3Float(v.positionX, v.positionY, v.positionZ)); + } } public void RenderRange(int offset, int count) diff --git a/MatterControlLib/ApplicationView/Config/BedConfig.cs b/MatterControlLib/ApplicationView/Config/BedConfig.cs index 99f9f05e6..0ca7fb4a7 100644 --- a/MatterControlLib/ApplicationView/Config/BedConfig.cs +++ b/MatterControlLib/ApplicationView/Config/BedConfig.cs @@ -542,6 +542,23 @@ namespace MatterHackers.MatterControl } } + internal AxisAlignedBoundingBox GetAabbOfRenderGCode3D() + { + if (this.RenderInfo != null) + { + // If needed, update the RenderType flags to match to current user selection + if (RendererOptions.IsDirty) + { + this.RenderInfo.RefreshRenderType(); + RendererOptions.IsDirty = false; + } + + return this.GCodeRenderer.GetAabbOfRender3D(this.RenderInfo); + } + + return AxisAlignedBoundingBox.Empty(); + } + public void LoadActiveSceneGCode(string filePath, CancellationToken cancellationToken, Action progressReporter) { if (File.Exists(filePath)) diff --git a/MatterControlLib/DesignTools/EditorTools/RotateControls/PathControl.cs b/MatterControlLib/DesignTools/EditorTools/RotateControls/PathControl.cs index 4c5e88253..2ca502e88 100644 --- a/MatterControlLib/DesignTools/EditorTools/RotateControls/PathControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/RotateControls/PathControl.cs @@ -163,6 +163,15 @@ namespace MatterHackers.Plugins.EditorTools } } + AxisAlignedBoundingBox IObject3DControl.GetWorldspaceAABB() + { + // TODO: Untested. + if (flattened != null) + return new AxisAlignedBoundingBox(targets.Select(t => t.Point).ToArray()); + else + return AxisAlignedBoundingBox.Empty(); + } + private void Reset() { // Clear and close selection targets diff --git a/MatterControlLib/DesignTools/EditorTools/RotateControls/RotateCornerControl.cs b/MatterControlLib/DesignTools/EditorTools/RotateControls/RotateCornerControl.cs index a0618b9cb..58ab1e17b 100644 --- a/MatterControlLib/DesignTools/EditorTools/RotateControls/RotateCornerControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/RotateControls/RotateCornerControl.cs @@ -201,6 +201,27 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + IObject3D selectedItem = RootSelection; + if (selectedItem != null) + { + if (Object3DControlContext.SelectedObject3DControl == null) + { + box = AxisAlignedBoundingBox.Union(box, rotationHandle.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + + if (mouseMoveInfo != null || mouseDownInfo != null || MouseIsOver) + { + box = AxisAlignedBoundingBox.Union(box, GetRotationCompassAABB(selectedItem)); + } + } + + return box; + } + public Vector3 GetCornerPosition(IObject3D objectBeingRotated) { return GetCornerPosition(objectBeingRotated, out _); @@ -326,7 +347,7 @@ namespace MatterHackers.Plugins.EditorTools } var hitPlane = new PlaneShape(RotationPlanNormal, RotationPlanNormal.Dot(controlCenter), null); - IntersectInfo hitOnRotationPlane = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + IntersectInfo hitOnRotationPlane = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (hitOnRotationPlane != null) { AxisAlignedBoundingBox currentSelectedBounds = selectedItem.GetAxisAlignedBoundingBox(); @@ -645,6 +666,42 @@ namespace MatterHackers.Plugins.EditorTools } } + private AxisAlignedBoundingBox GetRotationCompassAABB(IObject3D selectedItem) + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + if (Object3DControlContext.Scene.SelectedItem == null) + { + return box; + } + + AxisAlignedBoundingBox currentSelectedBounds = selectedItem.GetAxisAlignedBoundingBox(); + if (currentSelectedBounds.XSize > 100000) + { + return box; + } + + if (mouseMoveInfo != null) + { + Matrix4X4 rotationCenterTransform = GetRotationTransform(selectedItem, out double radius); + + double innerRadius = radius + RingWidth / 2; + double outerRadius = innerRadius + RingWidth; + double snappingMarkRadius = outerRadius + 20; + + if (mouseDownInfo != null || MouseIsOver) + { + double snapMarkerRadius = GuiWidget.DeviceScale * 10; + double snapMarkerOuterRadius = snappingMarkRadius + snapMarkerRadius; + box = AxisAlignedBoundingBox.Union(box, new AxisAlignedBoundingBox(-snapMarkerOuterRadius, -snapMarkerOuterRadius, 0, snapMarkerOuterRadius, snapMarkerOuterRadius, 0).NewTransformed(rotationCenterTransform)); + } + } + + return box; + } + + + private bool ForceHideAngle() { return (Object3DControlContext.HoveredObject3DControl != this diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleDiameterControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleDiameterControl.cs index 102c0f2a0..7a3ed509f 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleDiameterControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleDiameterControl.cs @@ -171,15 +171,30 @@ namespace MatterHackers.Plugins.EditorTools Object3DControlContext.GuiSurface.BeforeDraw -= Object3DControl_BeforeDraw; } - public override void Draw(DrawGlContentEventArgs e) + private bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = controlVisible == null ? true : controlVisible(); if (Object3DControlContext.SelectedObject3DControl != null && Object3DControlContext.SelectedObject3DControl as ScaleDiameterControl == null) { - shouldDrawScaleControls = false; + return false; } + return true; + } + + private Matrix4X4 GetRingTransform() + { + Vector3 newBottomCenter = ObjectSpace.GetCenterPosition(RootSelection, placement); + var rotation = Matrix4X4.CreateRotation(new Quaternion(RootSelection.Matrix)); + var translation = Matrix4X4.CreateTranslation(newBottomCenter); + return rotation * translation; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; if (selectedItem != null) @@ -201,10 +216,7 @@ namespace MatterHackers.Plugins.EditorTools GLHelper.Render(grabControlMesh, color.WithAlpha(e.Alpha0to255), TotalTransform, RenderTypes.Shaded); } - Vector3 newBottomCenter = ObjectSpace.GetCenterPosition(selectedItem, placement); - var rotation = Matrix4X4.CreateRotation(new Quaternion(selectedItem.Matrix)); - var translation = Matrix4X4.CreateTranslation(newBottomCenter); - Object3DControlContext.World.RenderRing(rotation * translation, Vector3.Zero, getDiameters[diameterIndex](), 60, color.WithAlpha(e.Alpha0to255), 2, 0, e.ZBuffered); + Object3DControlContext.World.RenderRing(GetRingTransform(), Vector3.Zero, getDiameters[diameterIndex](), 60, color.WithAlpha(e.Alpha0to255), 2, 0, e.ZBuffered); } if (hitPlane != null) @@ -223,6 +235,35 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, grabControlMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + + var xform = GetRingTransform(); + var radius = getDiameters[diameterIndex]() / 2; + box = AxisAlignedBoundingBox.Union(box, new AxisAlignedBoundingBox(-radius, -radius, 0, radius, radius, 0).NewTransformed(xform)); + } + + if (shouldDrawScaleControls && (MouseIsOver || MouseDownOnControl)) + { + var (a, b, c, d) = GetMeasureLine(); + box = AxisAlignedBoundingBox.Union(box, new AxisAlignedBoundingBox(new Vector3[] { a, b, c, d })); + } + } + + return box; + } + public override void OnMouseDown(Mouse3DEventArgs mouseEvent3D) { var selectedItem = RootSelection; @@ -269,7 +310,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl && hitPlane != null) { - var info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + var info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleHeightControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleHeightControl.cs index f5af3648f..5a25eb1d5 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleHeightControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleHeightControl.cs @@ -194,10 +194,9 @@ namespace MatterHackers.Plugins.EditorTools Object3DControlContext.GuiSurface.BeforeDraw -= Object3DControl_BeforeDraw; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = true; - var selectedItem = RootSelection; if (Object3DControlContext.SelectedObject3DControl != null && Object3DControlContext.SelectedObject3DControl as ScaleHeightControl == null) @@ -205,6 +204,14 @@ namespace MatterHackers.Plugins.EditorTools shouldDrawScaleControls = false; } + return shouldDrawScaleControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + if (selectedItem != null) { if (shouldDrawScaleControls) @@ -251,6 +258,27 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, topScaleMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + + box.ExpandToInclude(GetTopPosition(selectedItem)); + box.ExpandToInclude(GetBottomPosition(selectedItem)); + } + + return box; + } + public Vector3 GetBottomPosition(IObject3D selectedItem) { var meshBounds = selectedItem.GetAxisAlignedBoundingBox(selectedItem.Matrix.Inverted); @@ -316,7 +344,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl) { - IntersectInfo info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + IntersectInfo info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixCornerControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixCornerControl.cs index 83cc85855..340f1621d 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixCornerControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixCornerControl.cs @@ -154,7 +154,7 @@ namespace MatterHackers.Plugins.EditorTools transformAppliedByThis = selectedItem.Matrix; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = true; if (Object3DControlContext.SelectedObject3DControl != null @@ -162,6 +162,12 @@ namespace MatterHackers.Plugins.EditorTools { shouldDrawScaleControls = false; } + return shouldDrawScaleControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); var selectedItem = RootSelection; @@ -201,6 +207,40 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + + if (selectedItem != null && Object3DControlContext.Scene.ShowSelectionShadow) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, minXminYMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + + Vector3 startPosition = GetCornerPosition(selectedItem, quadrantIndex); + Vector3 endPosition = GetCornerPosition(selectedItem, (quadrantIndex + 1) % 4); + box.ExpandToInclude(startPosition); + box.ExpandToInclude(endPosition); + + if (MouseIsOver || MouseDownOnControl) + { + var (a0, a1, a2, a3) = GetMeasureLine(selectedItem, quadrantIndex); + var (b0, b1, b2, b3) = GetMeasureLine(selectedItem, quadrantIndex + 1); + box = AxisAlignedBoundingBox.Union(box, new AxisAlignedBoundingBox(new Vector3[] { + a0, a1, a2, a3, + b0, b1, b2, b3, + })); + } + } + + return box; + } + + private (Vector3 start0, Vector3 end0, Vector3 start1, Vector3 end1) GetMeasureLine(IObject3D selectedItem, int quadrant) { var corner = new Vector3[4]; @@ -308,7 +348,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl && hitPlane != null) { - IntersectInfo info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + IntersectInfo info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixEdgeControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixEdgeControl.cs index cacb31699..cce8233fc 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixEdgeControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixEdgeControl.cs @@ -167,7 +167,7 @@ namespace MatterHackers.Plugins.EditorTools transformAppliedByThis = selectedItem.Matrix; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = true; if (Object3DControlContext.SelectedObject3DControl != null @@ -175,6 +175,12 @@ namespace MatterHackers.Plugins.EditorTools { shouldDrawScaleControls = false; } + return shouldDrawScaleControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); var selectedItem = RootSelection; @@ -200,6 +206,24 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, minXminYMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + } + + return box; + } + public Vector3 GetCornerPosition(IObject3D item, int quadrantIndex) { AxisAlignedBoundingBox originalSelectedBounds = item.GetAxisAlignedBoundingBox(); @@ -275,7 +299,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl && hitPlane != null) { - IntersectInfo info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + IntersectInfo info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixTopControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixTopControl.cs index 567e61dc8..32f9795c8 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixTopControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleMatrixTopControl.cs @@ -146,16 +146,22 @@ namespace MatterHackers.Plugins.EditorTools Object3DControlContext.GuiSurface.BeforeDraw += Object3DControl_BeforeDraw; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = true; - var selectedItem = RootSelection; if (Object3DControlContext.SelectedObject3DControl != null && Object3DControlContext.SelectedObject3DControl as ScaleMatrixTopControl == null) { shouldDrawScaleControls = false; } + return shouldDrawScaleControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; if (selectedItem != null) { @@ -205,6 +211,31 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, topScaleMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + + Vector3 topPosition = GetTopPosition(selectedItem); + var bottomPosition = topPosition; + var originalSelectedBounds = selectedItem.GetAxisAlignedBoundingBox(); + bottomPosition.Z = originalSelectedBounds.MinXYZ.Z; + box.ExpandToInclude(topPosition); + box.ExpandToInclude(bottomPosition); + } + + return box; + } + public Vector3 GetTopPosition(IObject3D selectedItem) { AxisAlignedBoundingBox originalSelectedBounds = selectedItem.GetAxisAlignedBoundingBox(); @@ -250,7 +281,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl) { - IntersectInfo info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + IntersectInfo info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthCornerControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthCornerControl.cs index be791dc77..9b7759fe1 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthCornerControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthCornerControl.cs @@ -170,7 +170,7 @@ namespace MatterHackers.Plugins.EditorTools Object3DControlContext.GuiSurface.BeforeDraw -= Object3DControl_BeforeDraw; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = true; if (Object3DControlContext.SelectedObject3DControl != null @@ -178,6 +178,12 @@ namespace MatterHackers.Plugins.EditorTools { shouldDrawScaleControls = false; } + return shouldDrawScaleControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); var selectedItem = RootSelection; @@ -222,6 +228,39 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, minXminYMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + + Vector3 startPosition = ObjectSpace.GetCornerPosition(selectedItem, quadrantIndex); + Vector3 endPosition = ObjectSpace.GetCornerPosition(selectedItem, (quadrantIndex + 1) % 4); + box.ExpandToInclude(startPosition); + box.ExpandToInclude(endPosition); + + if (MouseIsOver || MouseDownOnControl) + { + var (a0, a1, a2, a3) = GetMeasureLine(quadrantIndex); + var (b0, b1, b2, b3) = GetMeasureLine(quadrantIndex + 1); + box = AxisAlignedBoundingBox.Union(box, new AxisAlignedBoundingBox(new Vector3[] { + a0, a1, a2, a3, + b0, b1, b2, b3 + })); + } + } + + return box; + } + public override void OnMouseDown(Mouse3DEventArgs mouseEvent3D) { var selectedItem = RootSelection; @@ -272,7 +311,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl && hitPlane != null) { - var info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + var info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthEdgeControl.cs b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthEdgeControl.cs index ee8ae0823..74ae1c064 100644 --- a/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthEdgeControl.cs +++ b/MatterControlLib/DesignTools/EditorTools/ScaleControls/ScaleWidthDepthEdgeControl.cs @@ -195,7 +195,7 @@ namespace MatterHackers.Plugins.EditorTools Object3DControlContext.GuiSurface.BeforeDraw -= Object3DControl_BeforeDraw; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawScaleControls() { bool shouldDrawScaleControls = true; if (Object3DControlContext.SelectedObject3DControl != null @@ -203,6 +203,12 @@ namespace MatterHackers.Plugins.EditorTools { shouldDrawScaleControls = false; } + return shouldDrawScaleControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawScaleControls = ShouldDrawScaleControls(); var selectedItem = RootSelection; @@ -239,6 +245,32 @@ namespace MatterHackers.Plugins.EditorTools base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawScaleControls(); + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, minXminYMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + + if (MouseIsOver || MouseDownOnControl) + { + var (a0, a1, a2, a3) = GetMeasureLine(selectedItem); + box = AxisAlignedBoundingBox.Union(box, new AxisAlignedBoundingBox(new Vector3[] { + a0, a1, a2, a3 + })); + } + } + + return box; + } + public override void OnMouseDown(Mouse3DEventArgs mouseEvent3D) { var selectedItem = RootSelection; @@ -289,7 +321,7 @@ namespace MatterHackers.Plugins.EditorTools if (MouseDownOnControl && hitPlane != null) { - var info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + var info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null) diff --git a/MatterControlLib/DesignTools/Interfaces/IEditorDraw.cs b/MatterControlLib/DesignTools/Interfaces/IEditorDraw.cs index 7f8d2137f..ee97cf9c8 100644 --- a/MatterControlLib/DesignTools/Interfaces/IEditorDraw.cs +++ b/MatterControlLib/DesignTools/Interfaces/IEditorDraw.cs @@ -30,12 +30,16 @@ either expressed or implied, of the FreeBSD Project. using System.Collections.Generic; using MatterHackers.Agg.UI; using MatterHackers.MatterControl.PartPreviewWindow; +using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.DesignTools { public interface IEditorDraw { void DrawEditor(Object3DControlsLayer object3DControlLayer, DrawEventArgs e); + + /// The worldspace AABB of any 3D editing geometry drawn by DrawEditor. + AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer); } public interface ICustomEditorDraw : IEditorDraw diff --git a/MatterControlLib/DesignTools/Obsolete/CurveObject3D.cs b/MatterControlLib/DesignTools/Obsolete/CurveObject3D.cs index 9eba328eb..0ac26a1be 100644 --- a/MatterControlLib/DesignTools/Obsolete/CurveObject3D.cs +++ b/MatterControlLib/DesignTools/Obsolete/CurveObject3D.cs @@ -93,6 +93,19 @@ namespace MatterHackers.MatterControl.DesignTools GL.Enable(EnableCap.Lighting); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + if (layer.Scene.SelectedItem != null + && layer.Scene.SelectedItem.DescendantsAndSelf().Where((i) => i == this).Any()) + { + var currentMatrixInv = Matrix.Inverted; + var aabb = this.GetAxisAlignedBoundingBox(currentMatrixInv); + return AxisAlignedBoundingBox.CenteredBox(new Vector3(Diameter, Diameter, aabb.ZSize), new Vector3(rotationCenter, aabb.Center.Z)).NewTransformed(this.WorldMatrix()); + } + + return AxisAlignedBoundingBox.Empty(); + } + public override void OnInvalidate(InvalidateArgs invalidateArgs) { if ((invalidateArgs.InvalidateType.HasFlag(InvalidateType.Children) diff --git a/MatterControlLib/DesignTools/Obsolete/FitToBoundsObject3D.cs b/MatterControlLib/DesignTools/Obsolete/FitToBoundsObject3D.cs index c8a4ea562..20d367320 100644 --- a/MatterControlLib/DesignTools/Obsolete/FitToBoundsObject3D.cs +++ b/MatterControlLib/DesignTools/Obsolete/FitToBoundsObject3D.cs @@ -253,24 +253,32 @@ namespace MatterHackers.MatterControl.DesignTools.Operations switch (MaintainRatio) { - case MaintainRatio.None: - break; - case MaintainRatio.X_Y: - var minXy = Math.Min(scale.X, scale.Y); - scale.X = minXy; - scale.Y = minXy; - break; - case MaintainRatio.X_Y_Z: - var minXyz = Math.Min(Math.Min(scale.X, scale.Y), scale.Z); - scale.X = minXyz; - scale.Y = minXyz; - scale.Z = minXyz; - break; + case MaintainRatio.None: + break; + case MaintainRatio.X_Y: + var minXy = Math.Min(scale.X, scale.Y); + scale.X = minXy; + scale.Y = minXy; + break; + case MaintainRatio.X_Y_Z: + var minXyz = Math.Min(Math.Min(scale.X, scale.Y), scale.Z); + scale.X = minXyz; + scale.Y = minXyz; + scale.Z = minXyz; + break; } ScaleItem.Matrix = Object3DExtensions.ApplyAtPosition(ScaleItem.Matrix, aabb.Center, Matrix4X4.CreateScale(scale)); } + AxisAlignedBoundingBox CalcBoxBounds(AxisAlignedBoundingBox itemAABB) + { + var center = itemAABB.Center; + var minXyz = center - new Vector3(Width / 2, Depth / 2, Height / 2); + var maxXyz = center + new Vector3(Width / 2, Depth / 2, Height / 2); + return new AxisAlignedBoundingBox(minXyz, maxXyz); + } + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) { if (layer.Scene.SelectedItem != null @@ -280,16 +288,11 @@ namespace MatterHackers.MatterControl.DesignTools.Operations if (FitType == FitType.Box) { - var center = aabb.Center; var worldMatrix = this.WorldMatrix(); - - var minXyz = center - new Vector3(Width / 2, Depth / 2, Height / 2); - var maxXyz = center + new Vector3(Width / 2, Depth / 2, Height / 2); - var bounds = new AxisAlignedBoundingBox(minXyz, maxXyz); // var leftW = Vector3Ex.Transform(, worldMatrix); - var right = Vector3Ex.Transform(center + new Vector3(Width / 2, 0, 0), worldMatrix); + //var right = Vector3Ex.Transform(center + new Vector3(Width / 2, 0, 0), worldMatrix); // layer.World.Render3DLine(left, right, Agg.Color.Red); - layer.World.RenderAabb(bounds, worldMatrix, Agg.Color.Red, 1, 1); + layer.World.RenderAabb(CalcBoxBounds(aabb), worldMatrix, Agg.Color.Red, 1, 1); } else { @@ -301,6 +304,26 @@ namespace MatterHackers.MatterControl.DesignTools.Operations } } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + if (layer.Scene.SelectedItem != null + && layer.Scene.SelectedItem.DescendantsAndSelf().Where((i) => i == this).Any()) + { + var aabb = ItemToScale.GetAxisAlignedBoundingBox(); + + if (FitType == FitType.Box) + { + return CalcBoxBounds(aabb).NewTransformed(this.WorldMatrix()); + } + else + { + return AxisAlignedBoundingBox.CenteredBox(new Vector3(Diameter, Diameter, Height), aabb.Center).NewTransformed(this.WorldMatrix()); + } + } + + return AxisAlignedBoundingBox.Empty(); + } + public void UpdateControls(PublicPropertyChange change) { change.SetRowVisible(nameof(Diameter), () => FitType != FitType.Box); diff --git a/MatterControlLib/DesignTools/Operations/ArrayRadialObject3D.cs b/MatterControlLib/DesignTools/Operations/ArrayRadialObject3D.cs index 8b5df5231..1b3ee76ab 100644 --- a/MatterControlLib/DesignTools/Operations/ArrayRadialObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/ArrayRadialObject3D.cs @@ -133,5 +133,10 @@ namespace MatterHackers.MatterControl.DesignTools.Operations { layer.World.RenderDirectionAxis(new DirectionAxis() { Normal = Axis.Normal, Origin = Vector3.Zero }, this.WorldMatrix(), 30); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return WorldViewExtensions.GetWorldspaceAabbOfRenderDirectionAxis(new DirectionAxis() { Normal = Axis.Normal, Origin = Vector3.Zero }, this.WorldMatrix(), 30); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/CurveObject3D_2.cs b/MatterControlLib/DesignTools/Operations/CurveObject3D_2.cs index 986c035aa..29204b4be 100644 --- a/MatterControlLib/DesignTools/Operations/CurveObject3D_2.cs +++ b/MatterControlLib/DesignTools/Operations/CurveObject3D_2.cs @@ -75,27 +75,54 @@ namespace MatterHackers.MatterControl.DesignTools [Description("Split the mesh so it has enough geometry to create a smooth curve")] public bool SplitMesh { get; set; } = true; - public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + struct DrawInfo + { + public AxisAlignedBoundingBox sourceAabb; + public double distance; + public Vector3 center; + } + + DrawInfo GetDrawInfo() { var sourceAabb = this.SourceContainer.GetAxisAlignedBoundingBox(); var distance = Diameter / 2 + sourceAabb.YSize / 2; var center = sourceAabb.Center + new Vector3(0, BendCcw ? distance : -distance, 0); center.X -= sourceAabb.XSize / 2 - (StartPercent / 100.0) * sourceAabb.XSize; + return new DrawInfo + { + sourceAabb = sourceAabb, + distance = distance, + center = center, + }; + } + + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + { + var drawInfo = GetDrawInfo(); + // render the top and bottom rings - layer.World.RenderCylinderOutline(this.WorldMatrix(), center, Diameter, sourceAabb.ZSize, 100, Color.Red, Color.Transparent); + layer.World.RenderCylinderOutline(this.WorldMatrix(), drawInfo.center, Diameter, drawInfo.sourceAabb.ZSize, 100, Color.Red, Color.Transparent); // render the split lines var radius = Diameter / 2; var circumference = MathHelper.Tau * radius; - var xxx = sourceAabb.XSize * (StartPercent / 100.0); + var xxx = drawInfo.sourceAabb.XSize * (StartPercent / 100.0); var startAngle = MathHelper.Tau * 3 / 4 - xxx / circumference * MathHelper.Tau; - layer.World.RenderCylinderOutline(this.WorldMatrix(), center, Diameter, sourceAabb.ZSize, (int)Math.Max(0, Math.Min(100, this.MinSidesPerRotation)), Color.Transparent, Color.Red, phase: startAngle); + layer.World.RenderCylinderOutline(this.WorldMatrix(), drawInfo.center, Diameter, drawInfo.sourceAabb.ZSize, (int)Math.Max(0, Math.Min(100, this.MinSidesPerRotation)), Color.Transparent, Color.Red, phase: startAngle); // turn the lighting back on GL.Enable(EnableCap.Lighting); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + var drawInfo = GetDrawInfo(); + var radius = Diameter / 2; + var halfHeight = drawInfo.sourceAabb.ZSize / 2; + return AxisAlignedBoundingBox.CenteredBox(new Vector3(radius, radius, halfHeight), drawInfo.center).NewTransformed(this.WorldMatrix()); + } + public override Task Rebuild() { this.DebugDepth("Rebuild"); diff --git a/MatterControlLib/DesignTools/Operations/CurveObject3D_3.cs b/MatterControlLib/DesignTools/Operations/CurveObject3D_3.cs index bdccc8ac6..6d3306109 100644 --- a/MatterControlLib/DesignTools/Operations/CurveObject3D_3.cs +++ b/MatterControlLib/DesignTools/Operations/CurveObject3D_3.cs @@ -102,11 +102,19 @@ namespace MatterHackers.MatterControl.DesignTools [DescriptionImage("https://lh3.googleusercontent.com/p9MyKu3AFP55PnobUKZQPqf6iAx11GzXyX-25f1ddrUnfCt8KFGd1YtHOR5HqfO0mhlX2ZVciZV4Yn0Kzfm43SErOS_xzgsESTu9scux")] public DoubleOrExpression MinSidesPerRotation { get; set; } = 30; - public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + struct DrawInfo + { + public double diameter; + public double startPercent; + public AxisAlignedBoundingBox sourceAabb; + public double distance; + public Vector3 center; + } + + DrawInfo GetDrawInfo() { var diameter = Diameter.Value(this); var startPercent = StartPercent.Value(this); - var minSidesPerRotation = MinSidesPerRotation.Value(this); var sourceAabb = this.SourceContainer.GetAxisAlignedBoundingBox(); var distance = diameter / 2 + sourceAabb.YSize / 2; @@ -114,20 +122,41 @@ namespace MatterHackers.MatterControl.DesignTools center.X -= sourceAabb.XSize / 2 - (startPercent / 100.0) * sourceAabb.XSize; center = Vector3.Zero;//.Transform(Matrix.Inverted); + return new DrawInfo + { + diameter = diameter, + startPercent = startPercent, + sourceAabb = sourceAabb, + distance = distance, + center = center, + }; + } + + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + { + var drawInfo = GetDrawInfo(); + var minSidesPerRotation = MinSidesPerRotation.Value(this); + // render the top and bottom rings - layer.World.RenderCylinderOutline(this.WorldMatrix(), center, diameter, sourceAabb.ZSize, 100, Color.Red, Color.Transparent); + layer.World.RenderCylinderOutline(this.WorldMatrix(), drawInfo.center, drawInfo.diameter, drawInfo.sourceAabb.ZSize, 100, Color.Red, Color.Transparent); // render the split lines - var radius = diameter / 2; + var radius = drawInfo.diameter / 2; var circumference = MathHelper.Tau * radius; - var xxx = sourceAabb.XSize * (startPercent / 100.0); + var xxx = drawInfo.sourceAabb.XSize * (drawInfo.startPercent / 100.0); var startAngle = MathHelper.Tau * 3 / 4 - xxx / circumference * MathHelper.Tau; - layer.World.RenderCylinderOutline(this.WorldMatrix(), center, diameter, sourceAabb.ZSize, (int)Math.Max(0, Math.Min(100, minSidesPerRotation)), Color.Transparent, Color.Red, phase: startAngle); + layer.World.RenderCylinderOutline(this.WorldMatrix(), drawInfo.center, drawInfo.diameter, drawInfo.sourceAabb.ZSize, (int)Math.Max(0, Math.Min(100, minSidesPerRotation)), Color.Transparent, Color.Red, phase: startAngle); // turn the lighting back on GL.Enable(EnableCap.Lighting); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + var drawInfo = GetDrawInfo(); + return AxisAlignedBoundingBox.CenteredBox(new Vector3(drawInfo.diameter, drawInfo.diameter, drawInfo.sourceAabb.ZSize), drawInfo.center).NewTransformed(this.WorldMatrix()); + } + private double DiameterFromAngle() { var diameter = Diameter.Value(this); diff --git a/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_2.cs b/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_2.cs index 393ce053c..a7d82ca28 100644 --- a/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_2.cs +++ b/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_2.cs @@ -150,16 +150,23 @@ namespace MatterHackers.MatterControl.DesignTools.Operations return fitToBounds; } - public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + AxisAlignedBoundingBox CalcBounds() { var aabb = UntransformedChildren.GetAxisAlignedBoundingBox(); var center = aabb.Center; - var worldMatrix = this.WorldMatrix(); - var minXyz = center - new Vector3(SizeX / 2, SizeY / 2, SizeZ / 2); var maxXyz = center + new Vector3(SizeX / 2, SizeY / 2, SizeZ / 2); - var bounds = new AxisAlignedBoundingBox(minXyz, maxXyz); - layer.World.RenderAabb(bounds, worldMatrix, Color.Red, 1, 1); + return new AxisAlignedBoundingBox(minXyz, maxXyz); + } + + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + { + layer.World.RenderAabb(this.CalcBounds(), this.WorldMatrix(), Color.Red, 1, 1); + } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.CalcBounds().NewTransformed(this.WorldMatrix()); } public override AxisAlignedBoundingBox GetAxisAlignedBoundingBox(Matrix4X4 matrix) diff --git a/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_3.cs b/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_3.cs index 995590c67..9940db610 100644 --- a/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_3.cs +++ b/MatterControlLib/DesignTools/Operations/FitToBoundsObject3D_3.cs @@ -111,11 +111,10 @@ namespace MatterHackers.MatterControl.DesignTools.Operations return fitToBounds; } - public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + AxisAlignedBoundingBox CalcBounds() { var aabb = UntransformedChildren.GetAxisAlignedBoundingBox(); var center = aabb.Center; - var worldMatrix = this.WorldMatrix(); var width = Width.Value(this); var depth = Depth.Value(this); @@ -123,8 +122,17 @@ namespace MatterHackers.MatterControl.DesignTools.Operations var minXyz = center - new Vector3(width / 2, depth / 2, height / 2); var maxXyz = center + new Vector3(width / 2, depth / 2, height / 2); - var bounds = new AxisAlignedBoundingBox(minXyz, maxXyz); - layer.World.RenderAabb(bounds, worldMatrix, Color.Red, 1, 1); + return new AxisAlignedBoundingBox(minXyz, maxXyz); + } + + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + { + layer.World.RenderAabb(this.CalcBounds(), this.WorldMatrix(), Color.Red, 1, 1); + } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return WorldViewExtensions.GetWorldspaceAabbOfRenderAabb(this.CalcBounds(), this.WorldMatrix(), 1, 1); } public override AxisAlignedBoundingBox GetAxisAlignedBoundingBox(Matrix4X4 matrix) diff --git a/MatterControlLib/DesignTools/Operations/FitToCylinderObject3D.cs b/MatterControlLib/DesignTools/Operations/FitToCylinderObject3D.cs index 616fe9824..7fb3e92c7 100644 --- a/MatterControlLib/DesignTools/Operations/FitToCylinderObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/FitToCylinderObject3D.cs @@ -112,6 +112,12 @@ namespace MatterHackers.MatterControl.DesignTools.Operations // layer.World.RenderCylinderOutline(Matrix4X4.Identity, Vector3.Zero, Diameter, aabb.ZSize, 30, Color.Green); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + var aabb = this.WorldAxisAlignedBoundingBox(); + return AxisAlignedBoundingBox.CenteredBox(new Vector3(Diameter, Diameter, aabb.ZSize), aabb.Center); + } + public override AxisAlignedBoundingBox GetAxisAlignedBoundingBox(Matrix4X4 matrix) { if (Children.Count == 2) diff --git a/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs b/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs index fe63731b3..6f4dddbf5 100644 --- a/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs @@ -214,6 +214,11 @@ namespace MatterHackers.MatterControl.DesignTools this.DrawPath(); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } + public override bool CanApply => true; public override void Apply(UndoBuffer undoBuffer) diff --git a/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D_2.cs b/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D_2.cs index fd6a81f1d..e5131ad50 100644 --- a/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D_2.cs +++ b/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D_2.cs @@ -185,6 +185,11 @@ namespace MatterHackers.MatterControl.DesignTools this.DrawPath(); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } + public override bool CanApply => true; [HideFromEditor] diff --git a/MatterControlLib/DesignTools/Operations/Image/PathObject3D.cs b/MatterControlLib/DesignTools/Operations/Image/PathObject3D.cs index 66bc37f35..dde7e6219 100644 --- a/MatterControlLib/DesignTools/Operations/Image/PathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Image/PathObject3D.cs @@ -33,6 +33,7 @@ using MatterHackers.DataConverters3D; using MatterHackers.MatterControl.DesignTools.Operations; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.PolygonMesh.Processors; +using MatterHackers.VectorMath; using Newtonsoft.Json; using System.Collections.Generic; @@ -59,5 +60,10 @@ namespace MatterHackers.MatterControl.DesignTools { this.DrawPath(); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs b/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs index bbf0da7bb..af339cf20 100644 --- a/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs +++ b/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs @@ -244,6 +244,40 @@ namespace MatterHackers.MatterControl.DesignTools.Operations } } + public static AxisAlignedBoundingBox GetWorldspaceAabbOfDrawPath(this IObject3D item) + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + if (item is IPathObject pathObject) + { + if (pathObject.VertexSource == null) + { + return box; + } + + var lastPosition = Vector2.Zero; + var maxXYZ = item.GetAxisAlignedBoundingBox().MaxXYZ; + maxXYZ = maxXYZ.Transform(item.Matrix.Inverted); + + foreach (var vertex in pathObject.VertexSource.Vertices()) + { + var position = vertex.position; + + if (vertex.IsLineTo) + { + box.ExpandToInclude(new Vector3(lastPosition, maxXYZ.Z + 0.002)); + box.ExpandToInclude(new Vector3(position, maxXYZ.Z + 0.002)); + } + + lastPosition = position; + } + + return box.NewTransformed(item.WorldMatrix()); + } + + return box; + } + public static bool IsRoot(this IObject3D object3D) { return object3D.Parent == null; diff --git a/MatterControlLib/DesignTools/Operations/Path/InflatePathObject3D.cs b/MatterControlLib/DesignTools/Operations/Path/InflatePathObject3D.cs index fbc2e692e..86d16cde6 100644 --- a/MatterControlLib/DesignTools/Operations/Path/InflatePathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Path/InflatePathObject3D.cs @@ -39,6 +39,7 @@ using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.PolygonMesh.Processors; +using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.DesignTools.Operations { @@ -141,5 +142,10 @@ namespace MatterHackers.MatterControl.DesignTools.Operations { this.DrawPath(); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/Path/MergePathObject3D.cs b/MatterControlLib/DesignTools/Operations/Path/MergePathObject3D.cs index da51e82f8..63c4a5500 100644 --- a/MatterControlLib/DesignTools/Operations/Path/MergePathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Path/MergePathObject3D.cs @@ -62,6 +62,11 @@ namespace MatterHackers.MatterControl.DesignTools.Operations this.DrawPath(); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } + public override bool CanApply => true; public override void Apply(UndoBuffer undoBuffer) diff --git a/MatterControlLib/DesignTools/Operations/Path/OutlinePathObject3D.cs b/MatterControlLib/DesignTools/Operations/Path/OutlinePathObject3D.cs index c1306e424..3752c8839 100644 --- a/MatterControlLib/DesignTools/Operations/Path/OutlinePathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Path/OutlinePathObject3D.cs @@ -41,6 +41,7 @@ using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.PolygonMesh.Processors; +using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.DesignTools.Operations { @@ -158,5 +159,10 @@ namespace MatterHackers.MatterControl.DesignTools.Operations { this.DrawPath(); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/Path/RevolveObject3D.cs b/MatterControlLib/DesignTools/Operations/Path/RevolveObject3D.cs index e0b1e1abd..57d973ac3 100644 --- a/MatterControlLib/DesignTools/Operations/Path/RevolveObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Path/RevolveObject3D.cs @@ -131,25 +131,42 @@ namespace MatterHackers.MatterControl.DesignTools.Operations } } + (Vector3, Vector3) GetStartEnd(IPathObject pathObject) + { + // draw the line that is the rotation point + var aabb = this.GetAxisAlignedBoundingBox(); + var vertexSource = this.VertexSource.Transform(Matrix); + var bounds = vertexSource.GetBounds(); + var lineX = bounds.Left + AxisPosition.Value(this); + + var start = new Vector3(lineX, aabb.MinXYZ.Y, aabb.MinXYZ.Z); + var end = new Vector3(lineX, aabb.MaxXYZ.Y, aabb.MinXYZ.Z); + return (start, end); + } + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) { var child = this.Children.FirstOrDefault(); if (child is IPathObject pathObject) { - // draw the line that is the rotation point - var aabb = this.GetAxisAlignedBoundingBox(); - var vertexSource = this.VertexSource.Transform(Matrix); - var bounds = vertexSource.GetBounds(); - var lineX = bounds.Left + AxisPosition.Value(this); - - var start = new Vector3(lineX, aabb.MinXYZ.Y, aabb.MinXYZ.Z); - var end = new Vector3(lineX, aabb.MaxXYZ.Y, aabb.MinXYZ.Z); - + var (start, end) = GetStartEnd(pathObject); layer.World.Render3DLine(start, end, Color.Red, true); layer.World.Render3DLine(start, end, Color.Red.WithAlpha(20), false); } } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + var child = this.Children.FirstOrDefault(); + if (child is IPathObject pathObject) + { + var (start, end) = GetStartEnd(pathObject); + return new AxisAlignedBoundingBox(new Vector3[] { start, end }); + } + + return AxisAlignedBoundingBox.Empty(); + } + private CancellationTokenSource cancellationToken; public bool IsBuilding => this.cancellationToken != null; diff --git a/MatterControlLib/DesignTools/Operations/Path/SmoothPathObject3D.cs b/MatterControlLib/DesignTools/Operations/Path/SmoothPathObject3D.cs index 40bb1fc42..87af4111b 100644 --- a/MatterControlLib/DesignTools/Operations/Path/SmoothPathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Path/SmoothPathObject3D.cs @@ -40,6 +40,7 @@ using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.PolygonMesh.Processors; +using MatterHackers.VectorMath; using Polygon = System.Collections.Generic.List; using Polygons = System.Collections.Generic.List>; @@ -186,5 +187,10 @@ namespace MatterHackers.MatterControl.DesignTools.Operations { this.DrawPath(); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/RotateObject3D_2.cs b/MatterControlLib/DesignTools/Operations/RotateObject3D_2.cs index c6dd6bce6..3290cdab1 100644 --- a/MatterControlLib/DesignTools/Operations/RotateObject3D_2.cs +++ b/MatterControlLib/DesignTools/Operations/RotateObject3D_2.cs @@ -120,6 +120,18 @@ namespace MatterHackers.MatterControl.DesignTools.Operations } } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + if (layer.Scene.SelectedItem != null + && layer.Scene.SelectedItem.DescendantsAndSelf().Where((i) => i == this).Any()) + { + return WorldViewExtensions.GetWorldspaceAabbOfRenderDirectionAxis(RotateAbout, this.WorldMatrix(), 30); + } + + return AxisAlignedBoundingBox.Empty(); + } + + public override async void OnInvalidate(InvalidateArgs invalidateArgs) { if ((invalidateArgs.InvalidateType.HasFlag(InvalidateType.Children) diff --git a/MatterControlLib/DesignTools/Operations/ScaleObject3D.cs b/MatterControlLib/DesignTools/Operations/ScaleObject3D.cs index 920afff86..f5fe73341 100644 --- a/MatterControlLib/DesignTools/Operations/ScaleObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/ScaleObject3D.cs @@ -43,6 +43,7 @@ using MatterHackers.Localizations; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.RenderOpenGl; using MatterHackers.VectorMath; +using MatterHackers.RenderOpenGl; using Newtonsoft.Json; namespace MatterHackers.MatterControl.DesignTools.Operations @@ -208,6 +209,17 @@ namespace MatterHackers.MatterControl.DesignTools.Operations } } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + if (layer.Scene.SelectedItem != null + && layer.Scene.SelectedItem.DescendantsAndSelf().Where((i) => i == this).Any()) + { + return RenderOpenGl.WorldViewExtensions.GetWorldspaceAabbOfRenderAxis(ScaleAbout, this.WorldMatrix(), 30, 1); + } + + return AxisAlignedBoundingBox.Empty(); + } + public async override void OnInvalidate(InvalidateArgs invalidateArgs) { if ((invalidateArgs.InvalidateType.HasFlag(InvalidateType.Children) diff --git a/MatterControlLib/DesignTools/Operations/TwistObject3D.cs b/MatterControlLib/DesignTools/Operations/TwistObject3D.cs index 80fe4000d..598d7978d 100644 --- a/MatterControlLib/DesignTools/Operations/TwistObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/TwistObject3D.cs @@ -143,6 +143,14 @@ namespace MatterHackers.MatterControl.DesignTools GL.Enable(EnableCap.Lighting); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + var sourceAabb = this.SourceContainer.GetAxisAlignedBoundingBox(); + var rotationCenter = SourceContainer.GetSmallestEnclosingCircleAlongZ().Center + RotationOffset; + var center = new Vector3(rotationCenter.X, rotationCenter.Y, sourceAabb.Center.Z); + return AxisAlignedBoundingBox.CenteredBox(new Vector3(1, 1, sourceAabb.ZSize), center).NewTransformed(this.WorldMatrix()); + } + public override Task Rebuild() { this.DebugDepth("Rebuild"); diff --git a/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs b/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs index 038b81f24..6a06e6bdd 100644 --- a/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs @@ -456,17 +456,31 @@ namespace MatterHackers.MatterControl.DesignTools } } + Matrix4X4 CalcTransform() + { + var aabb = this.GetAxisAlignedBoundingBox(this.WorldMatrix()); + return this.WorldMatrix() * Matrix4X4.CreateTranslation(0, 0, CalculationHeight.Value(this) - aabb.MinXYZ.Z + ExtrusionHeight.Value(this)); + } + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) { if (OutlineIsFromMesh) { - var aabb = this.GetAxisAlignedBoundingBox(this.WorldMatrix()); - // ExtrusionHeight - layer.World.RenderPathOutline(this.WorldMatrix() * Matrix4X4.CreateTranslation(0, 0, CalculationHeight.Value(this) - aabb.MinXYZ.Z + ExtrusionHeight.Value(this)), VertexSource, Agg.Color.Red, 5); + layer.World.RenderPathOutline(CalcTransform(), VertexSource, Agg.Color.Red, 5); // turn the lighting back on GL.Enable(EnableCap.Lighting); } } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + if (OutlineIsFromMesh) + { + // TODO: Untested. + return layer.World.GetWorldspaceAabbOfRenderPathOutline(CalcTransform(), VertexSource, 5); + } + return AxisAlignedBoundingBox.Empty(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Primitives/BoxPathObject3D.cs b/MatterControlLib/DesignTools/Primitives/BoxPathObject3D.cs index 5669894f3..2ecf96a9d 100644 --- a/MatterControlLib/DesignTools/Primitives/BoxPathObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/BoxPathObject3D.cs @@ -72,6 +72,11 @@ namespace MatterHackers.MatterControl.DesignTools this.DrawPath(); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } + /// /// This is the actual serialized with that can use expressions /// diff --git a/MatterControlLib/DesignTools/Primitives/DescriptionObject3D.cs b/MatterControlLib/DesignTools/Primitives/DescriptionObject3D.cs index 14be2f8f1..dc6a9184d 100644 --- a/MatterControlLib/DesignTools/Primitives/DescriptionObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/DescriptionObject3D.cs @@ -264,10 +264,10 @@ namespace MatterHackers.MatterControl.DesignTools CreateWidgetIfRequired(controlLayer); markdownWidget.Visible = true; - var descrpition = Description.Replace("\\n", "\n"); - if (markdownWidget.Markdown != descrpition) + var description = Description.Replace("\\n", "\n"); + if (markdownWidget.Markdown != description) { - markdownWidget.Markdown = descrpition; + markdownWidget.Markdown = description; } markdownWidget.Width = width; @@ -298,6 +298,11 @@ namespace MatterHackers.MatterControl.DesignTools graphics2DOpenGL.RenderTransformedPath(transform, new Ellipse(0, 0, 5, 5), theme.PrimaryAccentColor, false); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return AxisAlignedBoundingBox.Empty(); + } + private void CreateWidgetIfRequired(Object3DControlsLayer controlLayer) { if (markdownWidget == null diff --git a/MatterControlLib/DesignTools/Primitives/MeasureToolObject3D.cs b/MatterControlLib/DesignTools/Primitives/MeasureToolObject3D.cs index 8e1c95ce2..e881d6031 100644 --- a/MatterControlLib/DesignTools/Primitives/MeasureToolObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/MeasureToolObject3D.cs @@ -284,6 +284,11 @@ namespace MatterHackers.MatterControl.DesignTools } } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return new AxisAlignedBoundingBox(new Vector3[] { worldStartPosition, worldEndPosition }); + } + private void CreateWidgetIfRequired(Object3DControlsLayer controlLayer) { if (containerWidget == null diff --git a/MatterControlLib/DesignTools/Primitives/SetTemperatureObject3D.cs b/MatterControlLib/DesignTools/Primitives/SetTemperatureObject3D.cs index 24ddc7ce6..9db9a76ad 100644 --- a/MatterControlLib/DesignTools/Primitives/SetTemperatureObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/SetTemperatureObject3D.cs @@ -173,5 +173,10 @@ namespace MatterHackers.MatterControl.DesignTools theme.TextColor); Mesh.PlaceTextureOnFaces(0, texture); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return AxisAlignedBoundingBox.Empty(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs b/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs index 80a88c777..83d33e5a6 100644 --- a/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs @@ -199,5 +199,10 @@ namespace MatterHackers.MatterControl.DesignTools { this.DrawPath(); } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } } } \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Primitives/TracedPositionObject3DControl.cs b/MatterControlLib/DesignTools/Primitives/TracedPositionObject3DControl.cs index 75c47f5d7..98c9d3d18 100644 --- a/MatterControlLib/DesignTools/Primitives/TracedPositionObject3DControl.cs +++ b/MatterControlLib/DesignTools/Primitives/TracedPositionObject3DControl.cs @@ -120,6 +120,11 @@ namespace MatterHackers.MatterControl.DesignTools } } + AxisAlignedBoundingBox IObject3DControl.GetWorldspaceAABB() + { + return shape.GetAxisAlignedBoundingBox().NewTransformed(ShapeMatrix()); + } + private Matrix4X4 ShapeMatrix() { var worldPosition = getPosition(); diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/AABBDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/AABBDrawable.cs index 733feb178..855fc4ea8 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/AABBDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/AABBDrawable.cs @@ -59,5 +59,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow world.RenderDebugAABB(e.Graphics2D, child.GetAxisAlignedBoundingBox()); } } + + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + // No 3D drawing. + return AxisAlignedBoundingBox.Empty(); + } } } \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/AxisIndicatorDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/AxisIndicatorDrawable.cs index f4e411a7c..34315c227 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/AxisIndicatorDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/AxisIndicatorDrawable.cs @@ -80,5 +80,17 @@ namespace MatterHackers.MatterControl.PartPreviewWindow GLHelper.Render(mesh.mesh, mesh.color); } } + + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + foreach (var mesh in meshes) + { + box = AxisAlignedBoundingBox.Union(box, mesh.mesh.GetAxisAlignedBoundingBox()); + } + + return box; + } } } \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/FloorDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/FloorDrawable.cs index d775feda8..22bac3e11 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/FloorDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/FloorDrawable.cs @@ -61,6 +61,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private bool loadingTextures = false; + private const int GridSize = 600; + public FloorDrawable(Object3DControlsLayer.EditorType editorType, ISceneContext sceneContext, Color buildVolumeColor, ThemeConfig theme) { this.buildVolumeColor = buildVolumeColor; @@ -119,7 +121,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } else { - int width = 600; + int width = GridSize; GL.Disable(EnableCap.Lighting); GL.Disable(EnableCap.CullFace); @@ -179,6 +181,34 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + public AxisAlignedBoundingBox GetWorldspaceAABB() + { + if (!sceneContext.RendererOptions.RenderBed) + { + return AxisAlignedBoundingBox.Empty(); + } + else if (editorType == Object3DControlsLayer.EditorType.Printer) + { + AxisAlignedBoundingBox box = sceneContext.Mesh != null ? sceneContext.Mesh.GetAxisAlignedBoundingBox() : AxisAlignedBoundingBox.Empty(); + + if (sceneContext.PrinterShape != null) + { + box = AxisAlignedBoundingBox.Union(box, sceneContext.PrinterShape.GetAxisAlignedBoundingBox()); + } + + if (sceneContext.BuildVolumeMesh != null && sceneContext.RendererOptions.RenderBuildVolume) + { + box = AxisAlignedBoundingBox.Union(box, sceneContext.BuildVolumeMesh.GetAxisAlignedBoundingBox()); + } + + return box; + } + else + { + return new AxisAlignedBoundingBox(-GridSize, -GridSize, 0, GridSize, GridSize, 0); + } + } + private void EnsureBedTexture(IObject3D selectedItem, bool clearToPlaceholderImage = true) { // Early exit for invalid cases diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/FrustumDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/FrustumDrawable.cs new file mode 100644 index 000000000..991a0d1fe --- /dev/null +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/FrustumDrawable.cs @@ -0,0 +1,104 @@ +using MatterHackers.Agg; +using MatterHackers.Agg.UI; +using MatterHackers.DataConverters3D; +using MatterHackers.PolygonMesh; +using MatterHackers.RenderOpenGl; +using MatterHackers.RenderOpenGl.OpenGl; +using MatterHackers.VectorMath; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MatterHackers.MatterControl.PartPreviewWindow +{ + public class FrustumDrawable : IDrawable + { + public FrustumDrawable() + { + } + + string IDrawable.Title => "Frustum visualization"; + + string IDrawable.Description => "When enabled, captures the current frustum and visualizes it."; + + bool _enabled = false; + + bool IDrawable.Enabled + { + get => _enabled; + set + { + _enabled = value; + meshes.Clear(); + } + } + + DrawStage IDrawable.DrawStage => DrawStage.TransparentContent; + + readonly List<(Mesh mesh, Color color)> meshes = new List<(Mesh, Color)>(); + + static Mesh GetMesh(Vector3 a, Vector3 b, Vector3 c, Vector3 d) + { + Mesh mesh = new Mesh(); + mesh.Vertices.Add(a); + mesh.Vertices.Add(b); + mesh.Vertices.Add(c); + mesh.Vertices.Add(d); + mesh.Faces.Add(0, 1, 2, mesh.Vertices); + mesh.Faces.Add(0, 2, 3, mesh.Vertices); + return mesh; + } + + void IDrawable.Draw(GuiWidget sender, DrawEventArgs e, Matrix4X4 itemMaxtrix, WorldView world) + { + if (meshes.Count == 0) + { + Vector3[] ndcCoords = new Vector3[] { + new Vector3(+1, +1, +1), // 0: far top right + new Vector3(-1, +1, +1), // 1: far top left + new Vector3(+1, -1, +1), // 2: far bottom right + new Vector3(-1, -1, +1), // 3: far bottom left + new Vector3(+1, +1, -1), // 4: near top right + new Vector3(-1, +1, -1), // 5: near top left + new Vector3(+1, -1, -1), // 6: near bottom right + new Vector3(-1, -1, -1), // 7: near bottom left + }; + + Vector3[] worldspaceCoords = ndcCoords.Select(p => world.NDCToViewspace(p).TransformPosition(world.InverseModelviewMatrix)).ToArray(); + + // X + meshes.Add((GetMesh(worldspaceCoords[1], worldspaceCoords[5], worldspaceCoords[7], worldspaceCoords[3]), Color.Red.WithAlpha(0.5))); + meshes.Add((GetMesh(worldspaceCoords[0], worldspaceCoords[2], worldspaceCoords[6], worldspaceCoords[4]), Color.Red.WithAlpha(0.5))); + + // Y + meshes.Add((GetMesh(worldspaceCoords[3], worldspaceCoords[7], worldspaceCoords[6], worldspaceCoords[2]), Color.Green.WithAlpha(0.5))); + meshes.Add((GetMesh(worldspaceCoords[1], worldspaceCoords[0], worldspaceCoords[4], worldspaceCoords[5]), Color.Green.WithAlpha(0.5))); + + // Z + meshes.Add((GetMesh(worldspaceCoords[0], worldspaceCoords[1], worldspaceCoords[3], worldspaceCoords[2]), Color.Blue.WithAlpha(0.5))); + meshes.Add((GetMesh(worldspaceCoords[4], worldspaceCoords[5], worldspaceCoords[7], worldspaceCoords[6]), Color.Blue.WithAlpha(0.5))); + } + + GL.Disable(EnableCap.Lighting); + + foreach (var mesh in meshes) + { + GLHelper.Render(mesh.mesh, mesh.color, forceCullBackFaces: false); + } + + GL.Enable(EnableCap.Lighting); + } + + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + var box = AxisAlignedBoundingBox.Empty(); + + foreach (var mesh in meshes) + { + box = AxisAlignedBoundingBox.Union(box, mesh.mesh.GetAxisAlignedBoundingBox()); + } + + return box; + } + } +} \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawable.cs index 57d8b246b..bf6f81d22 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawable.cs @@ -43,5 +43,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow DrawStage DrawStage { get; } void Draw(GuiWidget sender, DrawEventArgs e, Matrix4X4 itemMaxtrix, WorldView world); + + /// The worldspace AABB of the 3D geometry drawn by Draw. + AxisAlignedBoundingBox GetWorldspaceAABB(); } } diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawableItem.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawableItem.cs index 814b45636..e0f7f119e 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawableItem.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/IDrawableItem.cs @@ -44,5 +44,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow DrawStage DrawStage { get; } void Draw(GuiWidget sender, IObject3D item, bool isSelected, DrawEventArgs e, Matrix4X4 itemMaxtrix, WorldView world); + + /// The worldspace AABB of the 3D geometry drawn by Draw. + AxisAlignedBoundingBox GetWorldspaceAABB(IObject3D item, bool isSelected, WorldView world); } } diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/InspectedItemDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/InspectedItemDrawable.cs index 91f9ad827..7b9ec856b 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/InspectedItemDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/InspectedItemDrawable.cs @@ -72,5 +72,22 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } } + + public AxisAlignedBoundingBox GetWorldspaceAABB(IObject3D item, bool isSelected, WorldView world) + { + if (item == scene.DebugItem) + { + AxisAlignedBoundingBox box = WorldViewExtensions.GetWorldspaceAabbOfRenderAabb(item.GetAxisAlignedBoundingBox(), Matrix4X4.Identity, 1); + + if (item.Mesh != null) + { + box = AxisAlignedBoundingBox.Union(box, item.Mesh.GetAxisAlignedBoundingBox(item.WorldMatrix())); + } + + return box; + } + + return AxisAlignedBoundingBox.Empty(); + } } } \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/ItemTraceDataDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/ItemTraceDataDrawable.cs index 84ba4c867..93ec18e2f 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/ItemTraceDataDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/ItemTraceDataDrawable.cs @@ -84,6 +84,16 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } } + + public AxisAlignedBoundingBox GetWorldspaceAABB(IObject3D item, bool isSelected, WorldView world) + { + if (isSelected) + { + return item.GetAxisAlignedBoundingBox(); + } + + return AxisAlignedBoundingBox.Empty(); + } } public class BvhItemView diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/NormalsDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/NormalsDrawable.cs index 477259c87..c80abe01e 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/NormalsDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/NormalsDrawable.cs @@ -88,5 +88,17 @@ namespace MatterHackers.MatterControl.PartPreviewWindow world.Render3DLineNoPrep(frustum, transformed1, transformed1 + normal, Color.Red, 2); } } + + public AxisAlignedBoundingBox GetWorldspaceAABB(IObject3D item, bool isSelected, WorldView world) + { + if (!isSelected || item.Mesh?.Faces.Count <= 0) + { + return AxisAlignedBoundingBox.Empty(); + } + + AxisAlignedBoundingBox box = item.Mesh.GetAxisAlignedBoundingBox().NewTransformed(item.WorldMatrix()); + box.Expand(1); // Normal length. + return box; + } } } \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/Object3DControlBoundingBoxesDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/Object3DControlBoundingBoxesDrawable.cs new file mode 100644 index 000000000..05b434be8 --- /dev/null +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/Object3DControlBoundingBoxesDrawable.cs @@ -0,0 +1,75 @@ +using MatterHackers.Agg; +using MatterHackers.Agg.UI; +using MatterHackers.PolygonMesh; +using MatterHackers.RenderOpenGl; +using MatterHackers.RenderOpenGl.OpenGl; +using MatterHackers.VectorMath; +using System; +using System.Collections.Generic; + +namespace MatterHackers.MatterControl.PartPreviewWindow +{ + public class Object3DControlBoundingBoxesDrawable : IDrawable + { + string IDrawable.Title => "Object3DControlsLayer bounding boxes"; + + string IDrawable.Description => "When enabled, show all the bounding boxes reported by Object3DControlsLayer."; + + bool IDrawable.Enabled { get; set; } + + DrawStage IDrawable.DrawStage => DrawStage.TransparentContent; + + void IDrawable.Draw(GuiWidget sender, DrawEventArgs e, Matrix4X4 itemMaxtrix, WorldView world) + { + if (!(sender is Object3DControlsLayer layer)) + return; + + GLHelper.PrepareFor3DLineRender(false); + + var frustum = world.GetClippingFrustum(); + + Color color = Color.White; + + List aabbs = layer.MakeListOfObjectControlBoundingBoxes(); + aabbs.Add(layer.GetPrinterNozzleAABB()); + + foreach (var box in aabbs) + { + if (box.XSize < 0) + continue; + + Vector3[] v = box.GetCorners(); + + Tuple[] lines = new Tuple[]{ + new Tuple(v[0], v[1]), + new Tuple(v[2], v[3]), + new Tuple(v[0], v[3]), + new Tuple(v[1], v[2]), + new Tuple(v[4 + 0], v[4 + 1]), + new Tuple(v[4 + 2], v[4 + 3]), + new Tuple(v[4 + 0], v[4 + 3]), + new Tuple(v[4 + 1], v[4 + 2]), + new Tuple(v[0], v[4 + 0]), + new Tuple(v[1], v[4 + 1]), + new Tuple(v[2], v[4 + 2]), + new Tuple(v[3], v[4 + 3]), + }; + + foreach (var (start, end) in lines) + { + world.Render3DLineNoPrep(frustum, start, end, color); + //e.Graphics2D.DrawLine(color, world.GetScreenPosition(start), world.GetScreenPosition(end)); + } + } + + GL.Enable(EnableCap.Lighting); + GL.Enable(EnableCap.DepthTest); + } + + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + // Let's not recurse on this... + return AxisAlignedBoundingBox.Empty(); + } + } +} diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/SceneTraceDataDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/SceneTraceDataDrawable.cs index b0de9b62c..86fdf2517 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/SceneTraceDataDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/SceneTraceDataDrawable.cs @@ -71,5 +71,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow Object3DControlsLayer.RenderBounds(e, world, bvhIterator); } + + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + // No 3D drawing. + return AxisAlignedBoundingBox.Empty(); + } } } \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/SceneViewer/SelectedItemDrawable.cs b/MatterControlLib/PartPreviewWindow/SceneViewer/SelectedItemDrawable.cs index 7767f2229..7e2401fe1 100644 --- a/MatterControlLib/PartPreviewWindow/SceneViewer/SelectedItemDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/SceneViewer/SelectedItemDrawable.cs @@ -108,6 +108,35 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + public AxisAlignedBoundingBox GetWorldspaceAABB(IObject3D item, bool isSelected, WorldView world) + { + if (isSelected && scene.DrawSelection) + { + var scaleMatrix = CalcScaleMatrix(world, item); + return item.Mesh.GetAxisAlignedBoundingBox(scaleMatrix); + } + + return AxisAlignedBoundingBox.Empty(); + } + + private Matrix4X4 CalcScaleMatrix(WorldView world, IObject3D item) + { + // Expand the object + var worldMatrix = item.WorldMatrix(); + var worldBounds = item.Mesh.GetAxisAlignedBoundingBox(worldMatrix); + var worldCenter = worldBounds.Center; + double distBetweenPixelsWorldSpace = world.GetWorldUnitsPerScreenPixelAtPosition(worldCenter); + var pixelsAccross = worldBounds.Size / distBetweenPixelsWorldSpace; + var pixelsWant = pixelsAccross + Vector3.One * 4 * Math.Sqrt(2); + + var wantMm = pixelsWant * distBetweenPixelsWorldSpace; + + return worldMatrix.ApplyAtPosition(worldCenter, Matrix4X4.CreateScale( + wantMm.X / worldBounds.XSize, + wantMm.Y / worldBounds.YSize, + wantMm.Z / worldBounds.ZSize)); + } + private void RenderSelection(IObject3D item, Color selectionColor, WorldView world) { if (item.Mesh == null) @@ -121,19 +150,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow GL.CullFace(CullFaceMode.Front); // Expand the object - var worldMatrix = item.WorldMatrix(); - var worldBounds = item.Mesh.GetAxisAlignedBoundingBox(worldMatrix); - var worldCenter = worldBounds.Center; - double distBetweenPixelsWorldSpace = world.GetWorldUnitsPerScreenPixelAtPosition(worldCenter); - var pixelsAccross = worldBounds.Size / distBetweenPixelsWorldSpace; - var pixelsWant = pixelsAccross + Vector3.One * 4 * Math.Sqrt(2); - - var wantMm = pixelsWant * distBetweenPixelsWorldSpace; - - var scaleMatrix = worldMatrix.ApplyAtPosition(worldCenter, Matrix4X4.CreateScale( - wantMm.X / worldBounds.XSize, - wantMm.Y / worldBounds.YSize, - wantMm.Z / worldBounds.ZSize)); + var scaleMatrix = CalcScaleMatrix(world, item); GLHelper.Render(item.Mesh, selectionColor, diff --git a/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractAndReplaceObject3D_2.cs b/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractAndReplaceObject3D_2.cs index e8e1fbe5f..bde7577b9 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractAndReplaceObject3D_2.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractAndReplaceObject3D_2.cs @@ -108,6 +108,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow.View3D return; } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return AxisAlignedBoundingBox.Empty(); + } + public override async void OnInvalidate(InvalidateArgs invalidateType) { if ((invalidateType.InvalidateType.HasFlag(InvalidateType.Children) diff --git a/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractObject3D_2.cs b/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractObject3D_2.cs index 6bf434efa..9d7dcbaf9 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractObject3D_2.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractObject3D_2.cs @@ -118,6 +118,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow.View3D return; } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return AxisAlignedBoundingBox.Empty(); + } + public override async void OnInvalidate(InvalidateArgs invalidateArgs) { if ((invalidateArgs.InvalidateType.HasFlag(InvalidateType.Children) diff --git a/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractPathObject3D.cs b/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractPathObject3D.cs index 136372511..799180541 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractPathObject3D.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Actions/SubtractPathObject3D.cs @@ -64,6 +64,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow.View3D this.DrawPath(); } + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + return this.GetWorldspaceAabbOfDrawPath(); + } + public void AddObject3DControls(Object3DControlsLayer object3DControlsLayer) { object3DControlsLayer.AddControls(ControlTypes.Standard2D); diff --git a/MatterControlLib/PartPreviewWindow/View3D/BedMeshGenerator.cs b/MatterControlLib/PartPreviewWindow/View3D/BedMeshGenerator.cs index 6e47c93e7..7ad11f838 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/BedMeshGenerator.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/BedMeshGenerator.cs @@ -122,12 +122,16 @@ namespace MatterHackers.MatterControl printerBed.Vertices[i] = printerBed.Vertices[i] - new Vector3Float(-printer.Bed.BedCenter, zTop + .02); } + printerBed.MarkAsChanged(); + if (buildVolume != null) { for (int i = 0; i < buildVolume.Vertices.Count; i++) { buildVolume.Vertices[i] = buildVolume.Vertices[i] - new Vector3Float(-printer.Bed.BedCenter, zTop + .02); } + + buildVolume.MarkAsChanged(); } return (printerBed, buildVolume); diff --git a/MatterControlLib/PartPreviewWindow/View3D/CameraFittingUtil.cs b/MatterControlLib/PartPreviewWindow/View3D/CameraFittingUtil.cs new file mode 100644 index 000000000..810cdb3f6 --- /dev/null +++ b/MatterControlLib/PartPreviewWindow/View3D/CameraFittingUtil.cs @@ -0,0 +1,758 @@ +// This isn't necessary for now as the orthographic camera is always an infinite distance away from the scene. +//#define ENABLE_ORTHOGRAPHIC_CAMERA_POSITIONING_ALONG_Z + +//#define ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP + +// If not defined, orthographic near/far fitting will use Mesh.Split. +// If defined, use the same method as perspective near/far fitting. Represent the AABB as solid shapes instead of faces. +//#define USE_TETRAHEDRON_CUTTING_FOR_ORTHOGRAPHIC_NEAR_FAR_FITTING + +using MatterHackers.Agg; +using MatterHackers.DataConverters3D; +using MatterHackers.PolygonMesh; +using MatterHackers.VectorMath; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MatterHackers +{ + // For "zoom to selection" and dynamic near/far. + internal static class CameraFittingUtil + { + /// + /// This proportion, scaled by the smaller dimension of the viewport, is subtracted from each side of the viewport for fitting. + /// Exposed for testing. + /// + public const double MarginScale = 0.1; + + public enum EPerspectiveFittingAlgorithm + { + /// + /// Has a margin, but does not use MarginScale. + /// + TrialAndError, + + /// + /// Fit the camera to the AABB's bounding sphere. + /// Guarantees a centered AABB center, but will not center the screenspace AABB. + /// + Sphere, + + /// + /// Place the camera at the center of the AABB, then push the camera back until all points are visible. + /// Guarantees a centered AABB center, but will not center the screenspace AABB. + /// + CenterOnWorldspaceAABB, + + /// + /// Fit the camera to the viewspace AABB, which will tend to be larger than the worldspace AABB. + /// Guarantees a centered AABB center, but will not center the screenspace AABB. + /// + CenterOnViewspaceAABB, + + /// + /// Take the perspective side planes and fit them around the AABB. + /// There will be two intersection lines and the camera will be placed on the one further back. + /// Either X or Y will be restrained to one value, and the other will take the value closest to the center of the AABB. + /// https://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene/66113254#66113254 + /// + IntersectionOfBoundingPlanesWithApproxCentering, + + /// + /// This is the only one that guarantees perfect screenspace centering. + /// A solver is used to center the screenspace AABB on the non-restrained axis. + /// But this means that the viewspace center will not always be at the center of the screen, so the object can sometimes look off-centered. + /// + IntersectionOfBoundingPlanesWithPerfectCentering, + } + + // Exposed for testing. + // "static readonly" to silence unreachable code warnings. + public static readonly EPerspectiveFittingAlgorithm PerspectiveFittingAlgorithm = EPerspectiveFittingAlgorithm.CenterOnWorldspaceAABB; + +#if ENABLE_ORTHOGRAPHIC_CAMERA_POSITIONING_ALONG_Z + // Scaled by the box's Z size in viewspace. + // If camera Z translation is needed and far - near > threshold, the camera will be placed close to the object. + const double OrthographicLargeZRangeScaledThreshold = 3.0; + const double OrthographicLargeZRangeScaledDistanceBetweenNearAndObject = 1.0; +#endif + + public struct Result + { + public Vector3 CameraPosition; + public double OrthographicViewspaceHeight; + } + + public static Result ComputeOrthographicCameraFit(WorldView world, double centerOffsetX, double zNear, double zFar, AxisAlignedBoundingBox worldspaceAABB) + { + Vector3[] worldspacePoints = worldspaceAABB.GetCorners(); + Vector3[] viewspacePoints = worldspacePoints.Select(x => x.TransformPosition(world.ModelviewMatrix)).ToArray(); + + Vector3 viewspaceCenter = worldspaceAABB.Center.TransformPosition(world.ModelviewMatrix); + AxisAlignedBoundingBox viewspaceAABB = new AxisAlignedBoundingBox(viewspaceCenter, viewspaceCenter); + foreach (Vector3 point in viewspacePoints) + { + viewspaceAABB.ExpandToInclude(point); + } + + // Take the viewport with margins subtracted, then fit the viewspace AABB to it. + Vector2 viewportSize = new Vector2(world.Width + centerOffsetX, world.Height); + double baseDim = Math.Min(viewportSize.X, viewportSize.Y); + double absTotalMargin = baseDim * MarginScale * 2; + Vector2 reducedViewportSize = viewportSize - new Vector2(absTotalMargin, absTotalMargin); + double unitsPerPixelX = viewspaceAABB.XSize / reducedViewportSize.X; + double unitsPerPixelY = viewspaceAABB.YSize / reducedViewportSize.Y; + double unitsPerPixel = Math.Max(unitsPerPixelX, unitsPerPixelY); + Vector2 targetViewspaceSize = viewportSize * unitsPerPixel; + + Vector3 viewspaceNearCenter = new Vector3(viewspaceAABB.Center.Xy, viewspaceAABB.MaxXYZ.Z); + Vector3 viewspaceCameraPosition = viewspaceNearCenter; + +#if ENABLE_ORTHOGRAPHIC_CAMERA_POSITIONING_ALONG_Z + Vector3 viewspaceFarCenter = new Vector3(viewspaceAABB.Center.Xy, viewspaceAABB.MinXYZ.Z); + if (-viewspaceNearCenter.Z >= zNear && -viewspaceFarCenter.Z <= zFar) + { + // The object fits in the Z range without translating along Z. + viewspaceCameraPosition.Z = 0; + } + else if (viewspaceAABB.ZSize * OrthographicLargeZRangeScaledThreshold < zFar - zNear) + { + // There's lots of Z range. + // Place the camera close to the object such that there's a reasonable amount of Z space behind and in front of the object. + viewspaceCameraPosition.Z += zNear + viewspaceAABB.ZSize * OrthographicLargeZRangeScaledDistanceBetweenNearAndObject; + } + else if (viewspaceAABB.ZSize < zFar - zNear) + { + // There's not much Z range, but enough to contain the object. + // Place the camera such that the object is in the middle of the Z range. + viewspaceCameraPosition.Z = viewspaceAABB.Center.Z + (zFar - zNear) * 0.5; + } + else + { + // The object is too big to fit in the Z range. + // Place the camera at the near side of the object. + viewspaceCameraPosition.Z = viewspaceAABB.MaxXYZ.Z; + } +#endif + + Vector3 worldspaceCameraPosition = viewspaceCameraPosition.TransformPosition(world.InverseModelviewMatrix); + return new Result { CameraPosition = worldspaceCameraPosition, OrthographicViewspaceHeight = targetViewspaceSize.Y }; + } + + public static Result ComputePerspectiveCameraFit(WorldView world, double centerOffsetX, AxisAlignedBoundingBox worldspaceAABB) + { + System.Diagnostics.Debug.Assert(!world.IsOrthographic); + + Vector3[] worldspacePoints = worldspaceAABB.GetCorners(); + Vector3[] viewspacePoints = worldspacePoints.Select(p => p.TransformPosition(world.ModelviewMatrix)).ToArray(); + Vector3 viewspaceCenter = viewspacePoints.Aggregate((a, b) => a + b) / viewspacePoints.Length; + + // Construct a temp WorldView with a smaller FOV to give the resulting view a margin. + Vector2 viewportSize = world.ViewportSize + new Vector2(centerOffsetX, 0); + double margin = MarginScale * Math.Min(viewportSize.X, viewportSize.Y); + WorldView reducedWorld = new WorldView(viewportSize.X - margin * 2, viewportSize.Y - margin * 2); + double reducedVFOVRadians = Math.Atan(reducedWorld.Height / world.Height * (world.NearPlaneHeightInViewspace / 2) / world.NearZ) * 2; + + reducedWorld.CalculatePerspectiveMatrixOffCenter( + reducedWorld.Width, reducedWorld.Height, + 0, + 1, 2, // Arbitrary + MathHelper.RadiansToDegrees(reducedVFOVRadians) + ); + + Plane[] viewspacePlanes = Frustum.FrustumFromProjectionMatrix(reducedWorld.ProjectionMatrix).Planes.Take(4).ToArray(); + + Vector3 viewspaceCameraPosition; + + switch (PerspectiveFittingAlgorithm) + { + case EPerspectiveFittingAlgorithm.TrialAndError: + return new Result { CameraPosition = TryPerspectiveCameraFitByIterativeAdjust(world, centerOffsetX, worldspaceAABB) }; + case EPerspectiveFittingAlgorithm.Sphere: + default: + viewspaceCameraPosition = PerspectiveCameraFitToSphere(reducedWorld, viewspaceCenter, viewspacePoints); + break; + case EPerspectiveFittingAlgorithm.CenterOnWorldspaceAABB: + viewspaceCameraPosition = PerspectiveCameraFitAlongAxisThroughCenter(viewspacePlanes, viewspaceCenter, viewspacePoints); + break; + case EPerspectiveFittingAlgorithm.CenterOnViewspaceAABB: + viewspaceCameraPosition = PerspectiveCameraFitToViewspaceAABB(viewspacePlanes, viewspaceCenter, viewspacePoints); + break; + case EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithApproxCentering: + viewspaceCameraPosition = PerspectiveCameraFitByAxisAlignedPlaneIntersections(reducedWorld, viewspacePlanes, viewspaceCenter, viewspacePoints, false); + break; + case EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithPerfectCentering: + viewspaceCameraPosition = PerspectiveCameraFitByAxisAlignedPlaneIntersections(reducedWorld, viewspacePlanes, viewspaceCenter, viewspacePoints, true); + break; + } + + return new Result { CameraPosition = viewspaceCameraPosition.TransformPosition(world.InverseModelviewMatrix) }; + } + + static bool NeedsToBeSmaller(RectangleDouble partScreenBounds, RectangleDouble goalBounds) + { + if (partScreenBounds.Bottom < goalBounds.Bottom + || partScreenBounds.Top > goalBounds.Top + || partScreenBounds.Left < goalBounds.Left + || partScreenBounds.Right > goalBounds.Right) + { + return true; + } + + return false; + } + + // Original code relocated from https://github.com/MatterHackers/MatterControl/blob/e5967ff858f2844734e4802a6c6c8ac973ad92d1/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs + static Vector3 TryPerspectiveCameraFitByIterativeAdjust(WorldView world, double centerOffsetX, AxisAlignedBoundingBox worldspaceAABB) + { + var aabb = worldspaceAABB; + var center = aabb.Center; + // pan to the center + var screenCenter = new Vector2(world.Width / 2 + centerOffsetX / 2, world.Height / 2); + var centerRay = world.GetRayForLocalBounds(screenCenter); + + // make the target size a portion of the total size + var goalBounds = new RectangleDouble(0, 0, world.Width, world.Height); + goalBounds.Inflate(-world.Width * .1); + + int rescaleAttempts = 0; + var testWorld = new WorldView(world.Width, world.Height); + testWorld.RotationMatrix = world.RotationMatrix; + var distance = 80.0; + + void AjustDistance() + { + testWorld.TranslationMatrix = world.TranslationMatrix; + var delta = centerRay.origin + centerRay.directionNormal * distance - center; + testWorld.Translate(delta); + } + + AjustDistance(); + + while (rescaleAttempts++ < 500) + { + + var partScreenBounds = testWorld.GetScreenBounds(aabb); + + if (NeedsToBeSmaller(partScreenBounds, goalBounds)) + { + distance++; + AjustDistance(); + partScreenBounds = testWorld.GetScreenBounds(aabb); + + // If it crossed over the goal reduct the amount we are adjusting by. + if (!NeedsToBeSmaller(partScreenBounds, goalBounds)) + { + break; + } + } + else + { + distance--; + AjustDistance(); + partScreenBounds = testWorld.GetScreenBounds(aabb); + + // If it crossed over the goal reduct the amount we are adjusting by. + if (NeedsToBeSmaller(partScreenBounds, goalBounds)) + { + break; + } + } + } + + //TrackballTumbleWidget.AnimateTranslation(center, centerRay.origin + centerRay.directionNormal * distance); + // zoom to fill the view + // viewControls3D.NotifyResetView(); + + return world.EyePosition - ((centerRay.origin + centerRay.directionNormal * distance) - center); + } + + static Vector3 PerspectiveCameraFitToSphere(WorldView world, Vector3 viewspaceCenter, Vector3[] viewspacePoints) + { + double radius = viewspacePoints.Select(p => (p - viewspaceCenter).Length).Max(); + double distForBT = radius / Math.Sin(MathHelper.DegreesToRadians(world.VFovDegrees) / 2); + double distForLR = radius / Math.Sin(MathHelper.DegreesToRadians(world.HFovDegrees) / 2); + double distForN = radius + WorldView.PerspectiveProjectionMinimumNearZ; + double dist = Math.Max(Math.Max(distForBT, distForLR), distForN); + return viewspaceCenter + new Vector3(0, 0, dist); + } + + + static Vector3 PerspectiveCameraFitAlongAxisThroughCenter(Plane[] viewspacePlanes, Vector3 viewspaceCenter, Vector3[] viewspacePoints) + { + Vector3 viewspaceCameraPosition = viewspaceCenter; + + viewspacePoints = viewspacePoints.Select(p => p - viewspaceCameraPosition).ToArray(); + + double relZ = double.NegativeInfinity; + + foreach (Plane viewspacePlane in viewspacePlanes) + { + relZ = Math.Max(relZ, viewspacePoints.Select( + p => p.Z - (viewspacePlane.DistanceFromOrigin - viewspacePlane.Normal.Dot(new Vector3(p.X, p.Y, 0))) / viewspacePlane.Normal.Z + ).Max()); + } + + return viewspaceCameraPosition + new Vector3(0, 0, relZ); + } + + static Vector3 PerspectiveCameraFitToViewspaceAABB(Plane[] viewspacePlanes, Vector3 viewspaceCenter, Vector3[] viewspacePoints) + { + AxisAlignedBoundingBox aabb = new AxisAlignedBoundingBox(viewspacePoints); + return PerspectiveCameraFitAlongAxisThroughCenter(viewspacePlanes, aabb.Center, aabb.GetCorners()); + } + + struct Line + { + public double gradient; + public double refX; + + public double YAt(double x) => gradient * (x - refX); + + public Line NegatedY() + { + return new Line { gradient = -gradient, refX = refX }; + } + } + + static double GetIntersectX(Line a, Line b, double minX, double maxX) + { + double x = (a.gradient * a.refX - b.gradient * b.refX) / (a.gradient - b.gradient); + if (x < minX) + return minX; + else if (x < maxX) + return x; + else // or, infinity, NaN + return double.PositiveInfinity; + } + + struct PiecewiseSegment + { + public Vector2 start; + public Line line; + + public PiecewiseSegment NegatedY() + { + return new PiecewiseSegment { start = new Vector2(start.X, -start.Y), line = line.NegatedY() }; + } + } + + static List SweepDescendingMaxY(double minX, double maxX, Line[] lines) + { + // Find the piecewise maximum of all the lines. + // NOTE: Monotonic decreasing Y, monotonic increasing gradient, max Y at minX. + + bool startsBelow(PiecewiseSegment seg, Line line) => seg.start.Y < line.YAt(seg.start.X); + + // Order segments by gradient, steep to shallow. + // For each line: + // Discard segments of the piecewise function that start below the line. + // Intersect and append a new segment. + + Array.Sort(lines, (a, b) => a.gradient.CompareTo(b.gradient)); + + var output = new List(); + + foreach (Line line in lines) + { + while (output.Count >= 1 && startsBelow(output.Last(), line)) + { + output.RemoveAt(output.Count - 1); + } + + if (output.Count == 0) + { + output.Add(new PiecewiseSegment { start = new Vector2(minX, line.YAt(minX)), line = line }); + } + else + { + double x = GetIntersectX(output.Last().line, line, output.Last().start.X, maxX); + if (output.Last().start.X < x && x < maxX) + output.Add(new PiecewiseSegment { start = new Vector2(x, line.YAt(x)), line = line }); + } + } + + return output; + } + + static List SweepDescendingMinY(double minX, double maxX, Line[] lines) + { + // Negate X and Y. + + // -f(-x) = gradient * (x - refX) + // -f(x) = gradient * (-x - refX) + // f(x) = gradient * (x + refX) + + List segs = SweepDescendingMaxY(-maxX, -minX, lines.Select(line => new Line + { + gradient = line.gradient, + refX = -line.refX + }).ToArray()).Select(seg => new PiecewiseSegment + { + start = -seg.start, + line = new Line { gradient = seg.line.gradient, refX = -seg.line.refX } + }).Reverse().ToList(); + + // The segs above have end points instead of start points. Shift them and set the start point. + for (int i = segs.Count - 1; i > 0; --i) + { + segs[i] = new PiecewiseSegment { start = segs[i - 1].start, line = segs[i].line }; + } + + segs[0] = new PiecewiseSegment { start = new Vector2(minX, segs[0].line.YAt(minX)), line = segs[0].line }; + + return segs; + } + + static Vector3 PerspectiveCameraFitByAxisAlignedPlaneIntersections( + WorldView world, Plane[] viewspacePlanes, Vector3 viewspaceCenter, Vector3[] viewspacePoints, + bool useSolver) + { + Plane[] viewspaceBoundingPlanes = viewspacePlanes.Select(plane => new Plane( + plane.Normal, + viewspacePoints.Select(point => plane.Normal.Dot(point)).Min() + )).ToArray(); + + double maxViewspaceZ = viewspacePoints.Select(p => p.Z).Max(); + + // Axis-aligned plane intersection as 2D line intersection: [a, b].[x or y, z] + c = 0 + Vector3 viewspaceLPlane2D = new Vector3(viewspaceBoundingPlanes[0].Normal.X, viewspaceBoundingPlanes[0].Normal.Z, -viewspaceBoundingPlanes[0].DistanceFromOrigin); + Vector3 viewspaceRPlane2D = new Vector3(viewspaceBoundingPlanes[1].Normal.X, viewspaceBoundingPlanes[1].Normal.Z, -viewspaceBoundingPlanes[1].DistanceFromOrigin); + Vector3 viewspaceBPlane2D = new Vector3(viewspaceBoundingPlanes[2].Normal.Y, viewspaceBoundingPlanes[2].Normal.Z, -viewspaceBoundingPlanes[2].DistanceFromOrigin); + Vector3 viewspaceTPlane2D = new Vector3(viewspaceBoundingPlanes[3].Normal.Y, viewspaceBoundingPlanes[3].Normal.Z, -viewspaceBoundingPlanes[3].DistanceFromOrigin); + + Vector3 intersectionLRInXZ = viewspaceLPlane2D.Cross(viewspaceRPlane2D); + Vector3 intersectionBTInYZ = viewspaceBPlane2D.Cross(viewspaceTPlane2D); + intersectionLRInXZ.Xy /= intersectionLRInXZ.Z; + intersectionBTInYZ.Xy /= intersectionBTInYZ.Z; + + double maxZByPlaneIntersections = Math.Max(intersectionLRInXZ.Y, intersectionBTInYZ.Y); + double maxZByNearPlane = maxViewspaceZ + WorldView.PerspectiveProjectionMinimumNearZ; + + // Initial position, before adjustment. + Vector3 viewspaceCameraPosition = new Vector3(intersectionLRInXZ.X, intersectionBTInYZ.X, Math.Max(maxZByPlaneIntersections, maxZByNearPlane)); + + double optimiseAxis(int axis, double min, double max) + { + if (!useSolver) + { + // Pick a point closest to viewspaceCenter. + return Math.Min(Math.Max(viewspaceCenter[axis], min), max); + } + + // [camX, camY, camZ] = viewspaceCameraPosition (the initial guess, with the final Z) + // ndcX = m[1,1] / (z - camZ) * (x - camX) + // ndcY = m[2,2] / (z - camZ) * (y - camY) + + Line[] ndcLines = viewspacePoints.Select(viewspacePoint => new Line + { + gradient = world.ProjectionMatrix[axis, axis] / (viewspacePoint.Z - viewspaceCameraPosition.Z), + refX = viewspacePoint[axis] + }).ToArray(); + + List piecewiseMax = SweepDescendingMaxY(min, max, ndcLines); + List piecewiseMin = SweepDescendingMinY(min, max, ndcLines); + +#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP + using (var file = new StreamWriter("perspective centering.csv")) + { + foreach (Line line in ndcLines) + { + double ndcAtMin = line.gradient * (min - line.refX); + double ndcAtMax = line.gradient * (max - line.refX); + file.WriteLine("{0}, {1}", min, ndcAtMin); + file.WriteLine("{0}, {1}", max, ndcAtMax); + } + + file.WriteLine(""); + + foreach (PiecewiseSegment seg in piecewiseMax) + file.WriteLine("{0}, {1}", seg.start.X, seg.start.Y); + file.WriteLine("{0}, {1}", max, piecewiseMax.Last().line.gradient * (max - piecewiseMax.Last().line.refX)); + + file.WriteLine(""); + + foreach (PiecewiseSegment seg in piecewiseMin) + file.WriteLine("{0}, {1}", seg.start.X, seg.start.Y); + file.WriteLine("{0}, {1}", max, piecewiseMin.Last().line.gradient * (max - piecewiseMin.Last().line.refX)); + } +#endif + + // Now, with the piecewise min and max functions, determine the X at which max == -min. + // Max is decreasing, -min is increasing. At some point, they should cross over. + + // Cross-over cannot be before minX. + if (piecewiseMax[0].start.Y <= -piecewiseMin[0].start.Y) + { + return min; + } + + int maxI = 0; + int minI = 0; + + double? resultX = null; + +#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP + using (var file = new StreamWriter("perspective piecewise crossover.csv")) +#endif + { + while (maxI < piecewiseMax.Count && minI < piecewiseMin.Count) + { + PiecewiseSegment maxSeg = piecewiseMax[maxI]; + PiecewiseSegment minSeg = piecewiseMin[minI].NegatedY(); + double maxSegEndX = maxI + 1 < piecewiseMax.Count ? piecewiseMax[maxI + 1].start.X : max; + double minSegEndX = minI + 1 < piecewiseMin.Count ? piecewiseMin[minI + 1].start.X : max; + double sectionMinX = Math.Max(maxSeg.start.X, minSeg.start.X); + double sectionMaxX = Math.Min(maxSegEndX, minSegEndX); + double crossoverX = GetIntersectX(maxSeg.line, minSeg.line, sectionMinX, sectionMaxX); + +#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP + file.WriteLine("{0}, {1}, {2}, {3}", sectionMinX, maxSeg.line.YAt(sectionMinX), sectionMinX, minSeg.line.YAt(sectionMinX)); +#endif + + if (crossoverX < sectionMaxX && !resultX.HasValue) + { + resultX = crossoverX; +#if !ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP + return resultX.Value; +#endif + } + + if (maxSegEndX < minSegEndX) + { + ++maxI; + } + else + { + ++minI; + } + } + +#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP + file.WriteLine("{0}, {1}, {2}, {3}", max, piecewiseMax.Last().line.YAt(max), max, piecewiseMin.Last().NegatedY().line.YAt(max)); +#endif + } + + return resultX ?? max; + } + + // Two axes are restrained to a single value. The last has a range of valid values. + if (intersectionLRInXZ.Y < intersectionBTInYZ.Y) + { + // The camera will be on the intersection of the top/bottom planes. + // The left/right planes in front intersect with the horizontal line and determine the limits of X. + double minX = (viewspaceRPlane2D.Y * intersectionBTInYZ.Y + viewspaceRPlane2D.Z) / -viewspaceRPlane2D.X; + double maxX = (viewspaceLPlane2D.Y * intersectionBTInYZ.Y + viewspaceLPlane2D.Z) / -viewspaceLPlane2D.X; + viewspaceCameraPosition.X = optimiseAxis(0, minX, maxX); + } + else + { + // The camera will be on the intersection of the left/right planes. + // The top/bottom planes in front intersect with the vertical line and determine the limits of Y. + double minY = (viewspaceTPlane2D.Y * intersectionLRInXZ.Y + viewspaceTPlane2D.Z) / -viewspaceTPlane2D.X; + double maxY = (viewspaceBPlane2D.Y * intersectionLRInXZ.Y + viewspaceBPlane2D.Z) / -viewspaceBPlane2D.X; + viewspaceCameraPosition.Y = optimiseAxis(1, minY, maxY); + } + + return viewspaceCameraPosition; + } + + /// Tetrahedron-based AABB-frustum intersection code for perspective projection dynamic near/far planes. + /// Temporary, may be replaced with a method that fits to individual triangles if needed. + /// This tetrahedron clipping is only used if + /// ENABLE_PERSPECTIVE_PROJECTION_DYNAMIC_NEAR_FAR is defined in View3DWidget.cs, or + /// USE_TETRAHEDRON_CUTTING_FOR_ORTHOGRAPHIC_NEAR_FAR_FITTING is defined. + + struct Tetrahedron + { + public Vector3 a, b, c, d; + } + + static Tetrahedron[] ClipTetrahedron(Tetrahedron T, Plane plane) + { + // true iff inside + Vector3[] vs = new Vector3[] { T.a, T.b, T.c, T.d }; + bool[] sides = vs.Select(v => plane.GetDistanceFromPlane(v) > 0).ToArray(); + int numInside = sides.Count(b => b); + + Vector3 temp; + + switch (numInside) + { + case 0: + default: + return new Tetrahedron[] { }; + + case 1: + { + int i = Array.IndexOf(sides, true); + (vs[0], vs[i]) = (vs[i], vs[0]); + temp = vs[0]; plane.ClipLine(ref temp, ref vs[1]); + temp = vs[0]; plane.ClipLine(ref temp, ref vs[2]); + temp = vs[0]; plane.ClipLine(ref temp, ref vs[3]); + // One tetra inside. + return new Tetrahedron[] { + new Tetrahedron{ a = vs[0], b = vs[1], c = vs[2], d = vs[3] }, + }; + } + + case 2: + { + int i = Array.IndexOf(sides, true); + (vs[0], vs[i]) = (vs[i], vs[0]); + (sides[0], sides[i]) = (sides[i], sides[0]); + int j = Array.IndexOf(sides, true, 1); + (vs[1], vs[j]) = (vs[j], vs[1]); + Vector3 v02 = vs[2]; + Vector3 v03 = vs[3]; + Vector3 v12 = vs[2]; + Vector3 v13 = vs[3]; + temp = vs[0]; plane.ClipLine(ref temp, ref v02); + temp = vs[0]; plane.ClipLine(ref temp, ref v03); + temp = vs[1]; plane.ClipLine(ref temp, ref v12); + temp = vs[1]; plane.ClipLine(ref temp, ref v13); + // Three new tetra sharing the common edge v03-v12. + return new Tetrahedron[] { + new Tetrahedron{ a = v12, b = v03, c = vs[0], d = v02 }, + new Tetrahedron{ a = v12, b = v03, c = vs[0], d = vs[1] }, + new Tetrahedron{ a = v12, b = v03, c = vs[1], d = v13 }, + }; + } + + case 3: + { + int i = Array.IndexOf(sides, false); + (vs[3], vs[i]) = (vs[i], vs[3]); + Vector3 v03 = vs[3]; + Vector3 v13 = vs[3]; + Vector3 v23 = vs[3]; + temp = vs[0]; plane.ClipLine(ref temp, ref v03); + temp = vs[1]; plane.ClipLine(ref temp, ref v13); + temp = vs[2]; plane.ClipLine(ref temp, ref v23); + // Three new tetra. + return new Tetrahedron[] { + new Tetrahedron{ a = vs[0], b = v03, c = v13, d = v23 }, + new Tetrahedron{ a = vs[0], b = vs[1], c = v13, d = v23 }, + new Tetrahedron{ a = vs[0], b = vs[1], c = vs[2], d = v23 }, + }; + } + + case 4: + return new Tetrahedron[] { T }; + } + } + + static readonly Tetrahedron[] BoxOfTetras = new Func(() => + { + Vector3[] corners = new Vector3[] { + new Vector3(+1, +1, +1), // [0] + new Vector3(-1, +1, +1), // [1] + new Vector3(+1, -1, +1), // [2] + new Vector3(-1, -1, +1), // [3] + new Vector3(+1, +1, -1), // [4] + new Vector3(-1, +1, -1), // [5] + new Vector3(+1, -1, -1), // [6] + new Vector3(-1, -1, -1), // [7] + }; + + // All the tetras share a common diagonal edge. + var box = new Tetrahedron[] { + new Tetrahedron{ a = corners[0], b = corners[7], c = corners[5], d = corners[4] }, + new Tetrahedron{ a = corners[0], b = corners[7], c = corners[4], d = corners[6] }, + new Tetrahedron{ a = corners[0], b = corners[7], c = corners[6], d = corners[2] }, + new Tetrahedron{ a = corners[0], b = corners[7], c = corners[2], d = corners[3] }, + new Tetrahedron{ a = corners[0], b = corners[7], c = corners[3], d = corners[1] }, + new Tetrahedron{ a = corners[0], b = corners[7], c = corners[1], d = corners[5] }, + }; + + // Sanity check. + double V = box.Select(T => Math.Abs((T.a - T.d).Dot((T.b - T.d).Cross(T.c - T.d)))).Sum(); + System.Diagnostics.Debug.Assert(MathHelper.AlmostEqual(V, 2 * 2 * 2 * 6, 1e-5)); + + return box; + })(); + + static Tetrahedron[] MakeAABBTetraArray(AxisAlignedBoundingBox box) + { + Vector3 halfsize = box.Size * 0.5; + Vector3 center = box.Center; + return BoxOfTetras.Select(T => new Tetrahedron + { + a = center + T.a * halfsize, + b = center + T.b * halfsize, + c = center + T.c * halfsize, + d = center + T.d * halfsize, + }).ToArray(); + } + + static Tetrahedron[] ClipTetras(Tetrahedron[] tetra, Plane plane) + { + return tetra.SelectMany(T => ClipTetrahedron(T, plane)).ToArray(); + } + + public static Tuple ComputeNearFarOfClippedWorldspaceAABB(bool isOrthographic, Plane[] worldspacePlanes, Matrix4X4 worldToViewspace, AxisAlignedBoundingBox worldspceAABB) + { + if (worldspceAABB == null || worldspceAABB.XSize < 0) + { + return null; + } + +#if !USE_TETRAHEDRON_CUTTING_FOR_ORTHOGRAPHIC_NEAR_FAR_FITTING + if (isOrthographic) + { + Mesh mesh = PlatonicSolids.CreateCube(worldspceAABB.Size); + mesh.Translate(worldspceAABB.Center); + + double tolerance = 0.001; + foreach (Plane plane in worldspacePlanes) + { + mesh.Split(plane, onPlaneDistance: tolerance, cleanAndMerge: false, discardFacesOnNegativeSide: true); + + // Remove any faces outside the plane (without using discardFacesOnNegativeSide). + //for (int i = mesh.Faces.Count - 1; i >= 0; --i) + //{ + // Face face = mesh.Faces[i]; + // double maxDist = new int[] { face.v0, face.v1, face.v2 }.Select(vi => plane.Normal.Dot(new Vector3(mesh.Vertices[vi]))).Max(); + // if (maxDist < (plane.DistanceFromOrigin < 0 ? plane.DistanceFromOrigin * (1 - 1e-4) : plane.DistanceFromOrigin * (1 + 1e-4))) + // { + // mesh.Faces[i] = mesh.Faces[mesh.Faces.Count - 1]; + // mesh.Faces.RemoveAt(mesh.Faces.Count - 1); + // } + //} + } + + mesh.CleanAndMerge(); + + if (mesh.Vertices.Any()) + { + mesh.Transform(worldToViewspace); + var depths = mesh.Vertices.Select(v => -v.Z); + return Tuple.Create(depths.Min(), depths.Max()); + } + } + else +#endif + { + // The above works for orthographic, but won't for perspective as the planes aren't parallel to Z. + // So, cut some tetrahedra instead. + + Tetrahedron[] tetras = MakeAABBTetraArray(worldspceAABB); + foreach (Plane plane in worldspacePlanes) + { + tetras = ClipTetras(tetras, plane); + } + + if (tetras.Any()) + { + var vertices = tetras.SelectMany(T => new Vector3[] { T.a, T.b, T.c, T.d }); + var depths = vertices.Select(v => -v.TransformPosition(worldToViewspace).Z).ToArray(); + return Tuple.Create(depths.Min(), depths.Max()); + } + } + + return null; + } + } +} diff --git a/MatterControlLib/PartPreviewWindow/View3D/Gui3D/MoveInZControl.cs b/MatterControlLib/PartPreviewWindow/View3D/Gui3D/MoveInZControl.cs index ecf15cc31..848c94d5c 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Gui3D/MoveInZControl.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Gui3D/MoveInZControl.cs @@ -156,7 +156,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow Object3DControlContext.GuiSurface.BeforeDraw -= Object3DControl_BeforeDraw; } - public override void Draw(DrawGlContentEventArgs e) + bool ShouldDrawMoveControls() { bool shouldDrawMoveControls = true; if (Object3DControlContext.SelectedObject3DControl != null @@ -164,6 +164,12 @@ namespace MatterHackers.MatterControl.PartPreviewWindow { shouldDrawMoveControls = false; } + return shouldDrawMoveControls; + } + + public override void Draw(DrawGlContentEventArgs e) + { + bool shouldDrawMoveControls = ShouldDrawMoveControls(); var selectedItem = RootSelection; if (selectedItem != null) @@ -234,7 +240,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow if (MouseDownOnControl && hitPlane != null) { - IntersectInfo info = hitPlane.GetClosestIntersection(mouseEvent3D.MouseRay); + IntersectInfo info = hitPlane.GetClosestIntersectionWithinRayDistanceRange(mouseEvent3D.MouseRay); if (info != null && selectedItem != null @@ -342,5 +348,24 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } } + + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + bool shouldDrawScaleControls = ShouldDrawMoveControls(); + var selectedItem = RootSelection; + + if (selectedItem != null) + { + if (shouldDrawScaleControls) + { + box = AxisAlignedBoundingBox.Union(box, upArrowMesh.GetAxisAlignedBoundingBox().NewTransformed(TotalTransform)); + } + } + + return box; + } + } } diff --git a/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SelectionShadow.cs b/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SelectionShadow.cs index 80a012ad1..e4ab1571c 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SelectionShadow.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SelectionShadow.cs @@ -85,6 +85,20 @@ namespace MatterHackers.MatterControl.PartPreviewWindow base.Draw(e); } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + var selectedItem = RootSelection; + if (selectedItem != null + && Object3DControlContext.Scene.ShowSelectionShadow) + { + AxisAlignedBoundingBox selectedBounds = selectedItem.GetAxisAlignedBoundingBox(); + var withScale = Matrix4X4.CreateScale(selectedBounds.XSize, selectedBounds.YSize, 1) * TotalTransform; + return GetNormalShadowMesh().GetAxisAlignedBoundingBox().NewTransformed(withScale); + } + + return AxisAlignedBoundingBox.Empty(); + } + public override void Dispose() { // no widgets allocated so nothing to close diff --git a/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SnappingIndicators.cs b/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SnappingIndicators.cs index 4ce61036c..9ffa8025d 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SnappingIndicators.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Gui3D/SnappingIndicators.cs @@ -139,6 +139,12 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + public override AxisAlignedBoundingBox GetWorldspaceAABB() + { + // No 3D drawing. + return AxisAlignedBoundingBox.Empty(); + } + public override void CancelOperation() { } diff --git a/MatterControlLib/PartPreviewWindow/View3D/Interaction/IObject3DControl.cs b/MatterControlLib/PartPreviewWindow/View3D/Interaction/IObject3DControl.cs index 99874df8a..e2d026f87 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Interaction/IObject3DControl.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Interaction/IObject3DControl.cs @@ -30,6 +30,7 @@ either expressed or implied, of the FreeBSD Project. using MatterHackers.DataConverters3D; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.RayTracer; +using MatterHackers.VectorMath; using System; namespace MatterHackers.MeshVisualizer @@ -65,6 +66,9 @@ namespace MatterHackers.MeshVisualizer bool DrawOnTop { get; } void Draw(DrawGlContentEventArgs e); + + /// The worldspace AABB of the 3D geometry drawn by Draw. + AxisAlignedBoundingBox GetWorldspaceAABB(); ITraceable GetTraceable(); } diff --git a/MatterControlLib/PartPreviewWindow/View3D/Interaction/Object3DControl.cs b/MatterControlLib/PartPreviewWindow/View3D/Interaction/Object3DControl.cs index 1f32f38c2..6a7115259 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Interaction/Object3DControl.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Interaction/Object3DControl.cs @@ -166,5 +166,8 @@ namespace MatterHackers.MeshVisualizer return null; } + + /// The worldspace AABB of the 3D geometry drawn by Draw. + public abstract AxisAlignedBoundingBox GetWorldspaceAABB(); } } \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/View3D/LevelingDataDrawable.cs b/MatterControlLib/PartPreviewWindow/View3D/LevelingDataDrawable.cs index 24156bb63..4ff83a4da 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/LevelingDataDrawable.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/LevelingDataDrawable.cs @@ -80,5 +80,15 @@ namespace MatterHackers.MatterControl.PartPreviewWindow darkWireframe); } } + + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + if (levelingDataMesh != null) + { + return levelingDataMesh.GetAxisAlignedBoundingBox(); + } + + return AxisAlignedBoundingBox.Empty(); + } } } diff --git a/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs b/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs index c327e171f..86a0dfac4 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs @@ -667,6 +667,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow { new AxisIndicatorDrawable(), new ScreenspaceAxisIndicatorDrawable(), + new FrustumDrawable(), + new Object3DControlBoundingBoxesDrawable(), new SceneTraceDataDrawable(sceneContext), new AABBDrawable(sceneContext), new LevelingDataDrawable(sceneContext), @@ -1030,6 +1032,12 @@ namespace MatterHackers.MatterControl.PartPreviewWindow return bCenterInViewSpace.LengthSquared.CompareTo(aCenterInViewSpace.LengthSquared); } + private Matrix4X4 GetEmulatorNozzleTransform() + { + var emulator = (PrinterEmulator.Emulator)sceneContext.Printer.Connection.serialPort; + return Matrix4X4.CreateTranslation(emulator.CurrentPosition + new Vector3(.5, .5, 5)); + } + private HashSet editorDrawItems = new HashSet(); private void DrawGlContent(DrawEventArgs e) { @@ -1095,7 +1103,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } - var matrix = Matrix4X4.CreateTranslation(emulator.CurrentPosition + new Vector3(.5, .5, 5)); + var matrix = GetEmulatorNozzleTransform(); GLHelper.Render(emulatorNozzleMesh, MaterialRendering.Color(sceneContext.Printer, emulator.ExtruderIndex), matrix, @@ -1255,5 +1263,71 @@ namespace MatterHackers.MatterControl.PartPreviewWindow WireframeAndSolid, None } + + public List MakeListOfObjectControlBoundingBoxes() + { + var selectedItem = scene.SelectedItem; + + var aabbs = new List(100); + + foreach (var ctrl in Object3DControls) + { + aabbs.Add(ctrl.GetWorldspaceAABB()); + } + + if (selectedItem is IEditorDraw editorDraw) + { + aabbs.Add(editorDraw.GetEditorWorldspaceAABB(this)); + } + + foreach (var ctrl in scene.Descendants()) + { + if (ctrl is ICustomEditorDraw customEditorDraw1 && customEditorDraw1.DoEditorDraw(ctrl == selectedItem)) + if (ctrl is IEditorDraw editorDraw2) + aabbs.Add(editorDraw2.GetEditorWorldspaceAABB(this)); + } + + foreach (var ctrl in drawables) + { + if (ctrl.Enabled) + aabbs.Add(ctrl.GetWorldspaceAABB()); + } + + foreach (var obj in scene.Children) + { + if (obj.Visible) + { + foreach (var item in obj.VisibleMeshes()) + { + bool isSelected = selectedItem != null + && (item == selectedItem + || item.Parents().Any(p => p == selectedItem)); + + // Invoke all item Drawables + foreach (var drawable in itemDrawables) + { + if (drawable.Enabled) + { + aabbs.Add(drawable.GetWorldspaceAABB(item, isSelected, this.World)); + } + } + } + } + } + + aabbs.Add(floorDrawable.GetWorldspaceAABB()); + + return aabbs; + } + + public AxisAlignedBoundingBox GetPrinterNozzleAABB() + { + if (sceneContext.Printer?.Connection?.serialPort is PrinterEmulator.Emulator emulator) + { + return emulatorNozzleMesh.GetAxisAlignedBoundingBox().NewTransformed(GetEmulatorNozzleTransform()); + } + + return AxisAlignedBoundingBox.Empty(); + } } } diff --git a/MatterControlLib/PartPreviewWindow/View3D/TrackballTumbleWidgetExtended.cs b/MatterControlLib/PartPreviewWindow/View3D/TrackballTumbleWidgetExtended.cs index f0942f5e6..b42808eaf 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/TrackballTumbleWidgetExtended.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/TrackballTumbleWidgetExtended.cs @@ -35,11 +35,17 @@ using MatterHackers.VectorMath; using MatterHackers.VectorMath.TrackBall; using System; using System.Collections.Generic; +using System.Linq; namespace MatterHackers.MatterControl.PartPreviewWindow { public class TrackballTumbleWidgetExtended : GuiWidget { + private const double PerspectiveMinZoomDist = 3; + private const double PerspectiveMaxZoomDist = 2300; + private const double OrthographicMinZoomViewspaceHeight = 0.01; + private const double OrthographicMaxZoomViewspaceHeight = 1000; + public NearFarAction GetNearFar; private readonly MotionQueue motionQueue = new MotionQueue(); private readonly GuiWidget sourceWidget; @@ -65,9 +71,10 @@ namespace MatterHackers.MatterControl.PartPreviewWindow this.world = world; this.sourceWidget = sourceWidget; this.Object3DControlLayer = Object3DControlLayer; + this.PerspectiveMode = !this.world.IsOrthographic; } - public delegate void NearFarAction(out double zNear, out double zFar); + public delegate void NearFarAction(WorldView world, out double zNear, out double zFar); public double CenterOffsetX { @@ -86,13 +93,55 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public TrackBallController TrackBallController { get; } public TrackBallTransformType TransformState { get; set; } public double ZoomDelta { get; set; } = 0.2f; + public double OrthographicZoomScalingFactor { get; set; } = 1.2f; public bool TurntableEnabled { get; set; } - public bool PerspectiveMode { get; set; } = true; + public bool PerspectiveMode { get; private set; } + // Projection mode switch animations will capture this value. When this is changed, those animations will cease to have an effect. + UInt64 _perspectiveModeSwitchAnimationSerialNumber = 0; + Action _perspectiveModeSwitchFinishAnimation = null; + + public void ChangeProjectionMode(bool perspective, bool animate) + { + FinishProjectionSwitch(); + + if (PerspectiveMode == perspective) + return; + + PerspectiveMode = perspective; + + if (!PerspectiveMode) + { + System.Diagnostics.Debug.Assert(!this.world.IsOrthographic); + if (!this.world.IsOrthographic) + { + // Perspective -> Orthographic + DoSwitchToProjectionMode(true, GetWorldRefPositionForProjectionSwitch(), animate); + } + } + else + { + System.Diagnostics.Debug.Assert(this.world.IsOrthographic); + if (this.world.IsOrthographic) + { + // Orthographic -> Perspective + DoSwitchToProjectionMode(false, GetWorldRefPositionForProjectionSwitch(), animate); + } + } + } + + private void FinishProjectionSwitch() + { + ++_perspectiveModeSwitchAnimationSerialNumber; + _perspectiveModeSwitchFinishAnimation?.Invoke(); + _perspectiveModeSwitchFinishAnimation = null; + } public void DoRotateAroundOrigin(Vector2 mousePosition) { if (isRotating) { + FinishProjectionSwitch(); + Quaternion activeRotationQuaternion; if (TurntableEnabled) { @@ -131,24 +180,29 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public override void OnDraw(Graphics2D graphics2D) { - RecalculateProjection(); + bool wantRefPositionVisibleForTransformInteraction = TrackBallController.CurrentTrackingType == TrackBallTransformType.None && ( + TransformState == TrackBallTransformType.Translation || + TransformState == TrackBallTransformType.Rotation + ); - if (TrackBallController.CurrentTrackingType == TrackBallTransformType.None) + bool isSwitchingProjectionMode = _perspectiveModeSwitchFinishAnimation != null; + + if (isSwitchingProjectionMode || wantRefPositionVisibleForTransformInteraction) { - switch (TransformState) - { - case TrackBallTransformType.Translation: - case TrackBallTransformType.Rotation: - var circle = new Ellipse(world.GetScreenPosition(mouseDownWorldPosition), 8 * DeviceScale); - graphics2D.Render(new Stroke(circle, 2 * DeviceScale), theme.PrimaryAccentColor); - graphics2D.Render(new Stroke(new Stroke(circle, 4 * DeviceScale), DeviceScale), theme.TextColor.WithAlpha(128)); - break; - } + var circle = new Ellipse(world.GetScreenPosition(mouseDownWorldPosition), 8 * DeviceScale); + graphics2D.Render(new Stroke(circle, 2 * DeviceScale), theme.PrimaryAccentColor); + graphics2D.Render(new Stroke(new Stroke(circle, 4 * DeviceScale), DeviceScale), theme.TextColor.WithAlpha(128)); } + base.OnDraw(graphics2D); } + public void OnBeforeDraw3D() + { + RecalculateProjection(); + } + public void OnDraw3D() { if (hitPlane != null) @@ -278,6 +332,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private void ZoomToMousePosition(Vector2 mousePosition, double zoomDelta) { + FinishProjectionSwitch(); + var rayAtScreenCenter = world.GetRayForLocalBounds(new Vector2(Width / 2, Height / 2)); var rayAtMousePosition = world.GetRayForLocalBounds(mousePosition); IntersectInfo intersectionInfo = Object3DControlLayer.Scene.GetBVHData().GetClosestIntersection(rayAtMousePosition); @@ -293,7 +349,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow // we did not hit anything // find a new 3d mouse position by hitting the screen plane at the distance of the last 3d mouse down position hitPlane = new PlaneShape(new Plane(rayAtScreenCenter.directionNormal, mouseDownWorldPosition), null); - intersectionInfo = hitPlane.GetClosestIntersection(rayAtMousePosition); + intersectionInfo = hitPlane.GetClosestIntersectionWithinRayDistanceRange(rayAtMousePosition); if (intersectionInfo != null) { ZoomToWorldPosition(intersectionInfo.HitPosition, zoomDelta); @@ -305,28 +361,38 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public void RecalculateProjection() { double trackingRadius = Math.Min(Width * .45, Height * .45); + + // TODO: Should probably be `Width / 2`, but currently has no effect? TrackBallController.ScreenCenter = new Vector2(Width / 2 - CenterOffsetX, Height / 2); TrackBallController.TrackBallRadius = trackingRadius; - var zNear = .01; - var zFar = 100.0; + double zNear = WorldView.DefaultNearZ; + double zFar = WorldView.DefaultFarZ; - GetNearFar?.Invoke(out zNear, out zFar); + Vector2 newViewportSize = new Vector2(Math.Max(1, sourceWidget.LocalBounds.Width), Math.Max(1, sourceWidget.LocalBounds.Height)); - if (CenterOffsetX != 0) + // Update the projection parameters for GetNearFar. + // NOTE: PerspectiveMode != this.world.IsOrthographic due to transition animations. + if (this.world.IsOrthographic) { - this.world.CalculatePerspectiveMatrixOffCenter(sourceWidget.Width, sourceWidget.Height, CenterOffsetX, zNear, zFar); - - if (!PerspectiveMode) - { - this.world.CalculatePerspectiveMatrixOffCenter(sourceWidget.Width, sourceWidget.Height, CenterOffsetX, zNear, zFar, 2); - //this.world.CalculateOrthogrphicMatrixOffCenter(sourceWidget.Width, sourceWidget.Height, CenterOffsetX, zNear, zFar); - } + this.world.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(newViewportSize.X, newViewportSize.Y, CenterOffsetX, this.world.NearPlaneHeightInViewspace, zNear, zFar); } else { - this.world.CalculatePerspectiveMatrix(sourceWidget.Width, sourceWidget.Height, zNear, zFar); + this.world.CalculatePerspectiveMatrixOffCenter(newViewportSize.X, newViewportSize.Y, CenterOffsetX, zNear, zFar, this.world.VFovDegrees); + } + + GetNearFar?.Invoke(this.world, out zNear, out zFar); + + // Use the updated near/far planes. + if (this.world.IsOrthographic) + { + this.world.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(newViewportSize.X, newViewportSize.Y, CenterOffsetX, this.world.NearPlaneHeightInViewspace, zNear, zFar); + } + else + { + this.world.CalculatePerspectiveMatrixOffCenter(newViewportSize.X, newViewportSize.Y, CenterOffsetX, zNear, zFar, this.world.VFovDegrees); } } @@ -338,6 +404,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public void SetRotationWithDisplacement(Quaternion rotationQ) { + FinishProjectionSwitch(); + if (isRotating) { ZeroVelocity(); @@ -348,6 +416,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public void StartRotateAroundOrigin(Vector2 mousePosition) { + FinishProjectionSwitch(); + if (isRotating) { ZeroVelocity(); @@ -360,6 +430,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public void Translate(Vector2 position) { + FinishProjectionSwitch(); + if (isRotating) { ZeroVelocity(); @@ -368,7 +440,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow if (hitPlane != null) { var rayAtPosition = world.GetRayForLocalBounds(position); - var hitAtPosition = hitPlane.GetClosestIntersection(rayAtPosition); + var hitAtPosition = hitPlane.GetClosestIntersectionWithinRayDistanceRange(rayAtPosition); if (hitAtPosition != null) { @@ -389,10 +461,13 @@ namespace MatterHackers.MatterControl.PartPreviewWindow UiThread.ClearInterval(runningInterval); } EndRotateAroundOrigin(); + FinishProjectionSwitch(); } public void ZoomToWorldPosition(Vector3 worldPosition, double zoomDelta) { + FinishProjectionSwitch(); + if (isRotating) { ZeroVelocity(); @@ -400,20 +475,94 @@ namespace MatterHackers.MatterControl.PartPreviewWindow // calculate the vector between the camera and the intersection position and move the camera along it by ZoomDelta, then set it's direction var delta = worldPosition - world.EyePosition; - var deltaLength = delta.Length; - var minDist = 3; - var maxDist = 2300; - if ((deltaLength < minDist && zoomDelta < 0) - || (deltaLength > maxDist && zoomDelta > 0)) + + if (this.world.IsOrthographic) { - return; + bool isZoomIn = zoomDelta < 0; + double scaleFactor = isZoomIn ? 1 / OrthographicZoomScalingFactor : OrthographicZoomScalingFactor; + double newViewspaceHeight = this.world.NearPlaneHeightInViewspace * scaleFactor; + + if (isZoomIn + ? newViewspaceHeight < OrthographicMinZoomViewspaceHeight + : newViewspaceHeight > OrthographicMaxZoomViewspaceHeight) + { + newViewspaceHeight = this.world.NearPlaneHeightInViewspace; + } + + this.world.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(this.world.Width, this.world.Height, CenterOffsetX, + newViewspaceHeight, this.world.NearZ, this.world.FarZ); + + // Zero out the viewspace Z component. + delta = delta.TransformVector(this.world.ModelviewMatrix); + delta.Z = 0; + delta = delta.TransformVector(this.world.InverseModelviewMatrix); } + else + { + var deltaLength = delta.Length; + + if ((deltaLength < PerspectiveMinZoomDist && zoomDelta < 0) + || (deltaLength > PerspectiveMaxZoomDist && zoomDelta > 0)) + { + return; + } + } + var zoomVec = delta * zoomDelta; world.Translate(zoomVec); Invalidate(); } + public void ZoomToAABB(AxisAlignedBoundingBox box) + { + FinishProjectionSwitch(); + + if (isRotating) + ZeroVelocity(); + + if (world.IsOrthographic) + { + // Using fake values for near/far. + // ComputeOrthographicCameraFit may move the camera to wherever as long as the scene is centered, then + // GetNearFar will figure out the near/far planes in the next projection update. + CameraFittingUtil.Result result = CameraFittingUtil.ComputeOrthographicCameraFit(world, CenterOffsetX, 0, 1, box); + + WorldView tempWorld = new WorldView(world.Width, world.Height); + tempWorld.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(world.Width, world.Height, CenterOffsetX, result.OrthographicViewspaceHeight, 0, 1); + double endViewspaceHeight = tempWorld.NearPlaneHeightInViewspace; + double startViewspaceHeight = world.NearPlaneHeightInViewspace; + + AnimateOrthographicTranslationAndHeight( + world.EyePosition, startViewspaceHeight, + result.CameraPosition, endViewspaceHeight + ); + } + else + { + CameraFittingUtil.Result result = CameraFittingUtil.ComputePerspectiveCameraFit(world, CenterOffsetX, box); + AnimateTranslation(result.CameraPosition, world.EyePosition); + } + } + + // Used for testing. + public RectangleDouble WorldspaceAabbToBottomScreenspaceRectangle(AxisAlignedBoundingBox box) + { + var points = box.GetCorners().Select(v => this.world.WorldspaceToBottomScreenspace(v).Xy); + var rect = new RectangleDouble(points.First(), points.First()); + foreach (Vector2 v in points.Skip(1)) + { + rect.ExpandToInclude(v); + } + return rect; + } + + // Used for testing. + public Vector3 WorldspaceToBottomScreenspace(Vector3 v) + { + return this.world.WorldspaceToBottomScreenspace(v); + } + private void ApplyVelocity() { if (isRotating) @@ -443,8 +592,15 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + private Vector3 GetWorldRefPositionForProjectionSwitch() + { + return mouseDownWorldPosition; + } + private void CalculateMouseDownPostionAndPlane(Vector2 mousePosition) { + FinishProjectionSwitch(); + var rayAtMousePosition = world.GetRayForLocalBounds(mousePosition); var intersectionInfo = Object3DControlLayer.Scene.GetBVHData().GetClosestIntersection(rayAtMousePosition); var rayAtScreenCenter = world.GetRayForLocalBounds(new Vector2(Width / 2, Height / 2)); @@ -459,7 +615,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow // we did not hit anything // find a new 3d mouse position by hitting the screen plane at the distance of the last 3d mouse down position hitPlane = new PlaneShape(new Plane(rayAtScreenCenter.directionNormal, mouseDownWorldPosition), null); - intersectionInfo = hitPlane.GetClosestIntersection(rayAtMousePosition); + intersectionInfo = hitPlane.GetClosestIntersectionWithinRayDistanceRange(rayAtMousePosition); + if (intersectionInfo != null) { mouseDownWorldPosition = intersectionInfo.HitPosition; @@ -495,6 +652,103 @@ namespace MatterHackers.MatterControl.PartPreviewWindow }, after); } + // To orthographic: + // Translate the camera towards infinity by maintaining an invariant perspective plane in worldspace and reducing the FOV to zero. + // The animation will switch to true orthographic at the end. + // To perspective: + // Translate the camera from infinity by maintaining an invariant perspective plane in worldspace and increasing the FOV to the default. + // The animation will switch out of orthographic in the first frame. + private void DoSwitchToProjectionMode( + bool toOrthographic, + Vector3 worldspaceRefPosition, + bool animate) + { + ZeroVelocity(); + + System.Diagnostics.Debug.Assert(toOrthographic != this.world.IsOrthographic); // Starting in the correct projection mode. + System.Diagnostics.Debug.Assert(_perspectiveModeSwitchFinishAnimation == null); // No existing animation. + + Matrix4X4 originalViewToWorld = this.world.InverseModelviewMatrix; + + Vector3 viewspaceRefPosition = worldspaceRefPosition.TransformPosition(this.world.ModelviewMatrix); + // Don't let this become negative when the ref position is behind the camera. + double refPlaneHeightInViewspace = Math.Abs(this.world.GetViewspaceHeightAtPosition(viewspaceRefPosition)); + double refZ = viewspaceRefPosition.Z; + + double refFOV = MathHelper.DegreesToRadians( + toOrthographic + ? this.world.VFovDegrees // start FOV + : WorldView.DefaultPerspectiveVFOVDegrees // end FOV + ); + + const int numUpdates = 10; + + var update = new Action((i) => + { + if (toOrthographic && i >= numUpdates) + { + world.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(world.Width, world.Height, CenterOffsetX, refPlaneHeightInViewspace, 0, 1); + } + else + { + double t = i / (double)numUpdates; + double fov = toOrthographic ? refFOV * (1 - t) : refFOV * t; + + double dist = refPlaneHeightInViewspace / 2 / Math.Tan(fov / 2); + double eyeZ = refZ + dist; + + Vector3 viewspaceEyePosition = new Vector3(0, 0, eyeZ); + + //System.Diagnostics.Trace.WriteLine("{0} {1} {2}".FormatWith(fovDegrees, dist, eyeZ)); + + world.CalculatePerspectiveMatrixOffCenter(world.Width, world.Height, CenterOffsetX, WorldView.DefaultNearZ, WorldView.DefaultFarZ, MathHelper.RadiansToDegrees(fov)); + world.EyePosition = viewspaceEyePosition.TransformPosition(originalViewToWorld); + } + }); + + if (animate) + { + _perspectiveModeSwitchFinishAnimation = () => + { + update(numUpdates); + }; + + UInt64 serialNumber = ++_perspectiveModeSwitchAnimationSerialNumber; + + Animation.Run(this, 0.25, numUpdates, (i) => + { + if (serialNumber == _perspectiveModeSwitchAnimationSerialNumber) + { + update(i); + if (i >= numUpdates) + { + _perspectiveModeSwitchFinishAnimation = null; + } + } + }, null); + } + else + { + update(numUpdates); + } + } + + private void AnimateOrthographicTranslationAndHeight( + Vector3 startCameraPosition, double startViewspaceHeight, + Vector3 endCameraPosition, double endViewspaceHeight, + Action after = null) + { + ZeroVelocity(); + Animation.Run(this, .25, 10, (update) => + { + double t = update / 10.0; + world.EyePosition = Vector3.Lerp(startCameraPosition, endCameraPosition, t); + // Arbitrary near/far planes. The next projection update will re-fit them. + double height = startViewspaceHeight * (1 - t) + endViewspaceHeight * t; + world.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(world.Width, world.Height, CenterOffsetX, height, 0, 1); + }, after); + } + internal class MotionQueue { private readonly List motionQueue = new List(); diff --git a/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs b/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs index 3fca1cc42..891ab1f22 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs @@ -26,7 +26,8 @@ The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ -// #define INCLUDE_ORTHOGRAPHIC +#define INCLUDE_ORTHOGRAPHIC +#define ENABLE_PERSPECTIVE_PROJECTION_DYNAMIC_NEAR_FAR using AngleSharp.Dom; using AngleSharp.Html.Parser; @@ -46,6 +47,7 @@ using MatterHackers.MatterControl.Library; using MatterHackers.MatterControl.PrinterCommunication; using MatterHackers.MatterControl.PrinterControls.PrinterConnections; using MatterHackers.MatterControl.SlicerConfiguration; +using MatterHackers.PolygonMesh; using MatterHackers.PolygonMesh.Processors; using MatterHackers.RayTracer; using MatterHackers.RenderOpenGl; @@ -66,6 +68,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow { public class View3DWidget : GuiWidget, IDrawable { + // Padded by this amount on each side, in case of unaccounted for scene geometry. + // For orthogrpahic, this is an offset on either side scaled by far - near. + // For perspective, this + 1 is used to scale the near and far planes. + private const double DynamicNearFarBoundsPaddingFactor = 0.1; + private bool deferEditorTillMouseUp = false; private bool expandSelection; @@ -470,6 +477,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow var zoomToSelectionButton = new IconButton(StaticData.Instance.LoadIcon("select.png", 16, 16).SetToColor(theme.TextColor), theme) { + Name = "Zoom to selection button", ToolTipText = "Zoom to Selection".Localize(), Margin = theme.ButtonSpacing }; @@ -519,27 +527,29 @@ namespace MatterHackers.MatterControl.PartPreviewWindow }; #if INCLUDE_ORTHOGRAPHIC - var perspectiveEnabled = UserSettings.Instance.get(UserSettingsKey.PerspectiveMode) != "False"; - TrackballTumbleWidget.PerspectiveMode = perspectiveEnabled; + var perspectiveEnabled = UserSettings.Instance.get(UserSettingsKey.PerspectiveMode) != false.ToString(); + TrackballTumbleWidget.ChangeProjectionMode(perspectiveEnabled, false); var projectionButton = new RadioIconButton(StaticData.Instance.LoadIcon("perspective.png", 16, 16).SetToColor(theme.TextColor), theme) { + Name = "Projection mode button", ToolTipText = "Perspective Mode".Localize(), Margin = theme.ButtonSpacing, ToggleButton = true, SiblingRadioButtonList = new List(), - Checked = turntableEnabled, + Checked = TrackballTumbleWidget.PerspectiveMode, }; AddRoundButton(projectionButton, RotatedMargin(projectionButton, -MathHelper.Tau * .3)); projectionButton.CheckedStateChanged += (s, e) => { UserSettings.Instance.set(UserSettingsKey.PerspectiveMode, projectionButton.Checked.ToString()); - TrackballTumbleWidget.PerspectiveMode = projectionButton.Checked; + TrackballTumbleWidget.ChangeProjectionMode(projectionButton.Checked, true); if (true) { // Make sure the view has up going the right direction // WIP, this should fix the current rotation rather than reset the view - ResetView(); + //ResetView(); } + Invalidate(); }; #endif @@ -672,81 +682,10 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public void ZoomToSelection() { - bool NeedsToBeSmaller(RectangleDouble partScreenBounds, RectangleDouble goalBounds) - { - if (partScreenBounds.Bottom < goalBounds.Bottom - || partScreenBounds.Top > goalBounds.Top - || partScreenBounds.Left < goalBounds.Left - || partScreenBounds.Right > goalBounds.Right) - { - return true; - } - - return false; - } - var selectedItem = this.Scene.SelectedItem; if (selectedItem != null) { - var aabb = selectedItem.GetAxisAlignedBoundingBox(); - var center = aabb.Center; - // pan to the center - var world = sceneContext.World; - var screenCenter = new Vector2(world.Width / 2 - selectedObjectPanel.Width / 2, world.Height / 2); - var centerRay = world.GetRayForLocalBounds(screenCenter); - - // make the target size a portion of the total size - var goalBounds = new RectangleDouble(0, 0, world.Width, world.Height); - goalBounds.Inflate(-world.Width * .1); - - int rescaleAttempts = 0; - var testWorld = new WorldView(world.Width, world.Height); - testWorld.RotationMatrix = world.RotationMatrix; - var distance = 80.0; - - void AjustDistance() - { - testWorld.TranslationMatrix = world.TranslationMatrix; - var delta = centerRay.origin + centerRay.directionNormal * distance - center; - testWorld.Translate(delta); - } - - AjustDistance(); - - while (rescaleAttempts++ < 500) - { - - var partScreenBounds = testWorld.GetScreenBounds(aabb); - - if (NeedsToBeSmaller(partScreenBounds, goalBounds)) - { - distance++; - AjustDistance(); - partScreenBounds = testWorld.GetScreenBounds(aabb); - - // If it crossed over the goal reduct the amount we are adjusting by. - if (!NeedsToBeSmaller(partScreenBounds, goalBounds)) - { - break; - } - } - else - { - distance--; - AjustDistance(); - partScreenBounds = testWorld.GetScreenBounds(aabb); - - // If it crossed over the goal reduct the amount we are adjusting by. - if (NeedsToBeSmaller(partScreenBounds, goalBounds)) - { - break; - } - } - } - - TrackballTumbleWidget.AnimateTranslation(center, centerRay.origin + centerRay.directionNormal * distance); - // zoom to fill the view - // viewControls3D.NotifyResetView(); + TrackballTumbleWidget.ZoomToAABB(selectedItem.GetAxisAlignedBoundingBox()); } } @@ -808,46 +747,71 @@ namespace MatterHackers.MatterControl.PartPreviewWindow }; } - private void GetNearFar(out double zNear, out double zFar) + private void GetNearFar(WorldView world, out double zNear, out double zFar) { - zNear = .1; - zFar = 100; + // All but the near and far planes. + Plane[] worldspacePlanes = Frustum.FrustumFromProjectionMatrix(world.ProjectionMatrix).Planes.Take(4) + .Select(p => p.Transform(world.InverseModelviewMatrix)).ToArray(); - // this function did not fix the image z fighting, so for now I'm just going to return rather than have it run. - return; - - var bounds = Scene.GetAxisAlignedBoundingBox(); - - if (bounds.XSize > 0) + // TODO: (Possibly) Compute a dynamic near plane based on visible triangles rather than clipped AABBs. + // Currently, the near and far planes are fit to clipped AABBs in the scene. + // A rudimentary implementation to start off with. The significant limitation is that zNear is ~zero when inside an AABB. + // The resulting frustum can be visualized using the debug menu. + // The far plane is less important. + // Other ideas are an infinite far plane, depth clamping, multi-pass rendering, AABB feedback from the previous frame, dedicated scene manager for all 3D rendering. + Tuple nearFar = null; + foreach (var aabb in Object3DControlLayer.MakeListOfObjectControlBoundingBoxes()) { - zNear = double.PositiveInfinity; - zFar = double.NegativeInfinity; - ExpandNearAndFarToBounds(ref zNear, ref zFar, bounds); - - // TODO: add in the bed bounds - - // TODO: add in the print volume bounds + nearFar = ExpandNearAndFarToClippedBounds(nearFar, world.IsOrthographic, worldspacePlanes, world.ModelviewMatrix, aabb); } + nearFar = ExpandNearAndFarToClippedBounds(nearFar, world.IsOrthographic, worldspacePlanes, world.ModelviewMatrix, Object3DControlLayer.GetPrinterNozzleAABB()); + nearFar = ExpandNearAndFarToClippedBounds(nearFar, world.IsOrthographic, worldspacePlanes, world.ModelviewMatrix, Scene.GetAxisAlignedBoundingBox()); + + zNear = nearFar != null ? nearFar.Item1 : WorldView.DefaultNearZ; + zFar = nearFar != null ? nearFar.Item2 : WorldView.DefaultFarZ; + + if (world.IsOrthographic) + { + WorldView.SanitiseOrthographicNearFar(ref zNear, ref zFar); + + // Add some padding in case of unaccounted geometry. + double padding = (zFar - zNear) * DynamicNearFarBoundsPaddingFactor; + zNear -= padding; + zFar += padding; + + WorldView.SanitiseOrthographicNearFar(ref zNear, ref zFar); + } + else + { +#if ENABLE_PERSPECTIVE_PROJECTION_DYNAMIC_NEAR_FAR + WorldView.SanitisePerspectiveNearFar(ref zNear, ref zFar); + + zNear /= 1 + DynamicNearFarBoundsPaddingFactor; + zFar *= 1 + DynamicNearFarBoundsPaddingFactor; +#else + zNear = WorldView.DefaultNearZ; + zFar = WorldView.DefaultFarZ; +#endif + WorldView.SanitisePerspectiveNearFar(ref zNear, ref zFar); + } + + System.Diagnostics.Debug.Assert(zNear < zFar && zFar < double.PositiveInfinity); } - private void ExpandNearAndFarToBounds(ref double zNear, ref double zFar, AxisAlignedBoundingBox bounds) + private static Tuple ExpandNearAndFarToClippedBounds(Tuple nearFar, bool isOrthographic, Plane[] worldspacePlanes, Matrix4X4 worldToViewspace, AxisAlignedBoundingBox bounds) { - for (int x = 0; x < 2; x++) + Tuple thisNearFar = CameraFittingUtil.ComputeNearFarOfClippedWorldspaceAABB(isOrthographic, worldspacePlanes, worldToViewspace, bounds); + if (nearFar == null) { - for (int y = 0; y < 2; y++) - { - for (int z = 0; z < 2; z++) - { - var cornerPoint = new Vector3((x == 0) ? bounds.MinXYZ.X : bounds.MaxXYZ.X, - (y == 0) ? bounds.MinXYZ.Y : bounds.MaxXYZ.Y, - (z == 0) ? bounds.MinXYZ.Z : bounds.MaxXYZ.Z); - - Vector3 viewPosition = cornerPoint.Transform(sceneContext.World.ModelviewMatrix); - - zNear = Math.Max(.1, Math.Min(zNear, -viewPosition.Z)); - zFar = Math.Max(Math.Max(zFar, -viewPosition.Z), zNear + .1); - } - } + return thisNearFar; + } + else if (thisNearFar == null) + { + return nearFar; + } + else + { + return Tuple.Create(Math.Min(nearFar.Item1, thisNearFar.Item1), Math.Max(nearFar.Item2, thisNearFar.Item2)); } } @@ -1385,6 +1349,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + TrackballTumbleWidget.OnBeforeDraw3D(); + base.OnDraw(graphics2D); } @@ -1784,7 +1750,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow Ray ray = sceneContext.World.GetRayForLocalBounds(localPosition); - return CurrentSelectInfo.HitPlane.GetClosestIntersection(ray); + return CurrentSelectInfo.HitPlane.GetClosestIntersectionWithinRayDistanceRange(ray); } public void DragSelectedObject(IObject3D selectedItem, Vector2 localMousePosition) @@ -1801,7 +1767,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow Vector2 meshViewerWidgetScreenPosition = this.Object3DControlLayer.TransformFromParentSpace(this, localMousePosition); Ray ray = sceneContext.World.GetRayForLocalBounds(meshViewerWidgetScreenPosition); - IntersectInfo info = CurrentSelectInfo.HitPlane.GetClosestIntersection(ray); + IntersectInfo info = CurrentSelectInfo.HitPlane.GetClosestIntersectionWithinRayDistanceRange(ray); if (info != null) { if (CurrentSelectInfo.LastMoveDelta == Vector3.PositiveInfinity) @@ -2314,6 +2280,37 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB() + { + AxisAlignedBoundingBox box = AxisAlignedBoundingBox.Empty(); + + if (CurrentSelectInfo.DownOnPart + && TrackballTumbleWidget.TransformState == TrackBallTransformType.None + && Keyboard.IsKeyDown(Keys.ShiftKey)) + { + var drawCenter = CurrentSelectInfo.PlaneDownHitPos; + + for (int i = 0; i < 2; i++) + { + box.ExpandToInclude(drawCenter - new Vector3(-50, 0, 0)); + box.ExpandToInclude(drawCenter - new Vector3(50, 0, 0)); + box.ExpandToInclude(drawCenter - new Vector3(0, -50, 0)); + box.ExpandToInclude(drawCenter - new Vector3(0, 50, 0)); + drawCenter.Z = 0; + } + } + + // Render 3D GCode if applicable + if (sceneContext.LoadedGCode != null + && sceneContext.GCodeRenderer != null + && printerTabPage?.Printer.ViewState.ViewMode == PartViewMode.Layers3D) + { + box = AxisAlignedBoundingBox.Union(box, printerTabPage.Printer.Bed.GetAabbOfRenderGCode3D()); + } + + return box; + } + string IDrawable.Title { get; } = "View3DWidget Extensions"; string IDrawable.Description { get; } = "Render axis indicators for shift drag and 3D GCode view"; diff --git a/MatterControlLib/PartPreviewWindow/View3D/WorldViewExtensions.cs b/MatterControlLib/PartPreviewWindow/View3D/WorldViewExtensions.cs index 032a03f38..81fbe9b68 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/WorldViewExtensions.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/WorldViewExtensions.cs @@ -89,5 +89,11 @@ namespace MatterHackers.MeshVisualizer GL.Enable(EnableCap.Lighting); } + + public static AxisAlignedBoundingBox GetWorldspaceAabbOfRenderDirectionAxis(DirectionAxis axis, Matrix4X4 matrix, double size) + { + double radius = axis.Normal.Length * size; + return AxisAlignedBoundingBox.CenteredHalfExtents(new Vector3(radius, radius, radius), axis.Origin).NewTransformed(matrix); + } } } diff --git a/Submodules/agg-sharp b/Submodules/agg-sharp index 7b9b67166..e71e8e1fc 160000 --- a/Submodules/agg-sharp +++ b/Submodules/agg-sharp @@ -1 +1 @@ -Subproject commit 7b9b6716605841d83d1ec8dbc4086e8235245896 +Subproject commit e71e8e1fcffba3de1cac98f2b78d0eb5ccc57178 diff --git a/Tests/MatterControl.AutomationTests/CameraFittingUtilTests.cs b/Tests/MatterControl.AutomationTests/CameraFittingUtilTests.cs new file mode 100644 index 000000000..5184358c0 --- /dev/null +++ b/Tests/MatterControl.AutomationTests/CameraFittingUtilTests.cs @@ -0,0 +1,192 @@ +using MatterHackers.Agg; +using MatterHackers.DataConverters3D; +using MatterHackers.GuiAutomation; +using MatterHackers.MatterControl.PartPreviewWindow; +using MatterHackers.VectorMath; +using NUnit.Framework; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MatterHackers.MatterControl.Tests.Automation +{ + [TestFixture, Category("MatterControl.UI.Automation"), RunInApplicationDomain, Apartment(ApartmentState.STA)] + public class CameraFittingUtilTests + { + private const string CoinName = "MatterControl - Coin.stl"; + + static Task DoZoomToSelectionTest(bool ortho, bool wideObject) + { + return MatterControlUtilities.RunTest(testRunner => + { + testRunner.OpenPartTab(removeDefaultPhil: wideObject); + + var view3D = testRunner.GetWidgetByName("View3DWidget", out _) as View3DWidget; + var scene = view3D.Object3DControlLayer.Scene; + + if (wideObject) + AddCoinToBed(testRunner, scene); + + if (ortho) + { + testRunner.ClickByName("Projection mode button"); + Assert.IsTrue(!view3D.TrackballTumbleWidget.PerspectiveMode); + testRunner.Delay(1); + Assert.IsTrue(!view3D.TrackballTumbleWidget.PerspectiveMode); + } + else + Assert.IsTrue(view3D.TrackballTumbleWidget.PerspectiveMode); + + Vector3[] lookAtDirFwds = new Vector3[] { + new Vector3(0, 0, -1), + new Vector3(0, 0, 1), + new Vector3(0, 1, 0), + new Vector3(1, 1, 0), + new Vector3(-1, -1, 0), + new Vector3(0, 1, 1), + new Vector3(1, 1, 1), + new Vector3(0, 1, -1), + new Vector3(1, 1, -1), + }; + + const int topI = 0; + const int bottomI = 1; + + for (int i = 0; i < lookAtDirFwds.Length; ++i) + { + Vector3 lookAtDirFwd = lookAtDirFwds[i]; + Vector3 lookAtDirRight = (i == topI ? -Vector3.UnitY : i == bottomI ? Vector3.UnitY : -Vector3.UnitZ).Cross(lookAtDirFwd); + Vector3 lookAtDirUp = lookAtDirRight.Cross(lookAtDirFwd).GetNormal(); + + var look = Matrix4X4.LookAt(Vector3.Zero, lookAtDirFwd, lookAtDirUp); + + view3D.TrackballTumbleWidget.AnimateRotation(look); + testRunner.Delay(0.5); + + testRunner.ClickByName("Zoom to selection button"); + testRunner.Delay(0.5); + + var part = testRunner.GetObjectByName(wideObject ? CoinName : "Phil A Ment.stl", out _) as IObject3D; + AxisAlignedBoundingBox worldspaceAABB = part.GetAxisAlignedBoundingBox(); + + Vector2 viewportSize = new Vector2(view3D.TrackballTumbleWidget.Width, view3D.TrackballTumbleWidget.Height); + RectangleDouble rect = view3D.TrackballTumbleWidget.WorldspaceAabbToBottomScreenspaceRectangle(worldspaceAABB); + Vector2 screenspacePositionOfWorldspaceCenter = view3D.TrackballTumbleWidget.WorldspaceToBottomScreenspace(worldspaceAABB.Center).Xy; + double marginPixels = CameraFittingUtil.MarginScale * Math.Min(viewportSize.X, viewportSize.Y); + + const double pixelTolerance = 1e-3; + + // Check that the full object is visible. + Assert.IsTrue(rect.Left > -pixelTolerance); + Assert.IsTrue(rect.Bottom > -pixelTolerance); + Assert.IsTrue(rect.Right < viewportSize.X + pixelTolerance); + Assert.IsTrue(rect.Top < viewportSize.Y + pixelTolerance); + + // Check for centering. + + bool isPerspectiveFittingWithinMargin = + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.Sphere || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.CenterOnWorldspaceAABB || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.CenterOnViewspaceAABB || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithApproxCentering || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithPerfectCentering; + + // Tightly bounded. At least one axis should be bounded by the margin. + bool isPerspectiveFittingBoundedByMargin = + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithApproxCentering || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithPerfectCentering; + + bool perspectiveFittingWillCenterTheAABBCenter = + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.Sphere || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.CenterOnWorldspaceAABB || + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.CenterOnViewspaceAABB; + + bool perspectiveFittingWillCenterTheScreenspaceAABB = + CameraFittingUtil.PerspectiveFittingAlgorithm == CameraFittingUtil.EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithPerfectCentering; + + // Always get the same result. + bool isPerspectiveFittingStable = + CameraFittingUtil.PerspectiveFittingAlgorithm != CameraFittingUtil.EPerspectiveFittingAlgorithm.TrialAndError; + + bool isXWorldspaceCentered = MathHelper.AlmostEqual(viewportSize.X / 2, screenspacePositionOfWorldspaceCenter.X, pixelTolerance); + bool isYWorldspaceCentered = MathHelper.AlmostEqual(viewportSize.Y / 2, screenspacePositionOfWorldspaceCenter.Y, pixelTolerance); + + bool isXMarginBounded = MathHelper.AlmostEqual(rect.Left, marginPixels, 1e-3) && MathHelper.AlmostEqual(rect.Right, viewportSize.X - marginPixels, pixelTolerance); + bool isYMarginBounded = MathHelper.AlmostEqual(rect.Bottom, marginPixels, 1e-3) && MathHelper.AlmostEqual(rect.Top, viewportSize.Y - marginPixels, pixelTolerance); + + bool isXWithinMargin = rect.Left > marginPixels - 1 && rect.Right < viewportSize.X - (marginPixels - 1); + bool isYWithinMargin = rect.Bottom > marginPixels - 1 && rect.Top < viewportSize.Y - (marginPixels - 1); + + bool isXScreenspaceCentered = MathHelper.AlmostEqual(viewportSize.X / 2, (rect.Left + rect.Right) / 2, pixelTolerance); + bool isYScreenspaceCentered = MathHelper.AlmostEqual(viewportSize.Y / 2, (rect.Bottom + rect.Top) / 2, pixelTolerance); + + if (ortho) + { + // Ortho fitting will always center the screenspace AABB and the center of the object AABB. + Assert.IsTrue(isXWorldspaceCentered && isYWorldspaceCentered); + Assert.IsTrue(isXMarginBounded || isYMarginBounded); + Assert.IsTrue(isXWithinMargin && isYWithinMargin); + Assert.IsTrue(isXScreenspaceCentered && isYScreenspaceCentered); + } + else + { + if (isPerspectiveFittingWithinMargin) + Assert.IsTrue(isXWithinMargin && isYWithinMargin); + + if (isPerspectiveFittingBoundedByMargin) + Assert.IsTrue(isXMarginBounded || isYMarginBounded); + + if (perspectiveFittingWillCenterTheAABBCenter) + Assert.IsTrue(isXWorldspaceCentered && isYWorldspaceCentered); + + if (perspectiveFittingWillCenterTheScreenspaceAABB) + Assert.IsTrue(isXScreenspaceCentered && isYScreenspaceCentered); + } + + if (ortho || isPerspectiveFittingStable) + { + testRunner.ClickByName("Zoom to selection button"); + testRunner.Delay(1); + + RectangleDouble rect2 = view3D.TrackballTumbleWidget.WorldspaceAabbToBottomScreenspaceRectangle(worldspaceAABB); + Assert.IsTrue(rect2.Equals(rect, pixelTolerance)); + } + } + + return Task.CompletedTask; + }, maxTimeToRun: 60 * 3, overrideWidth: 1300, overrideHeight: 800); + } + + [Test] + public Task OrthographicZoomToSelectionWide() + { + return DoZoomToSelectionTest(true, true); + } + + [Test] + public Task OrthographicZoomToSelectionTall() + { + return DoZoomToSelectionTest(true, false); + } + + [Test] + public Task PerspectiveZoomToSelectionWide() + { + return DoZoomToSelectionTest(false, true); + } + + [Test] + public Task PerspectiveZoomToSelectionTall() + { + return DoZoomToSelectionTest(false, false); + } + + private static void AddCoinToBed(AutomationRunner testRunner, InteractiveScene scene) + { + testRunner.AddItemToBed(partName: "Row Item MatterControl - Coin.stl") + .Delay(.1) + .ClickByName(CoinName, offset: new Point2D(-4, 0)); + Assert.IsNotNull(scene.SelectedItem); + } + } +} diff --git a/Tests/MatterControl.AutomationTests/MatterControl.AutomationTests.csproj b/Tests/MatterControl.AutomationTests/MatterControl.AutomationTests.csproj index a0da3491d..9363ba62a 100644 --- a/Tests/MatterControl.AutomationTests/MatterControl.AutomationTests.csproj +++ b/Tests/MatterControl.AutomationTests/MatterControl.AutomationTests.csproj @@ -53,6 +53,7 @@ MatterControlUtilities.cs +