Orthographic projection mode with dynamic near/far.

This commit is contained in:
fortsnek9348 2022-03-02 00:52:04 +00:00
parent 0834aff9f4
commit 8fafc54f90
70 changed files with 2548 additions and 244 deletions

View file

@ -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;
}
}
}

View file

@ -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); } }
/// <summary>
/// Create a new VertexBuffer
/// </summary>
@ -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)

View file

@ -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<double, string> progressReporter)
{
if (File.Exists(filePath))

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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);
/// <returns>The worldspace AABB of any 3D editing geometry drawn by DrawEditor.</returns>
AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer);
}
public interface ICustomEditorDraw : IEditorDraw

View file

@ -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)

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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");

View file

@ -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);

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -185,6 +185,11 @@ namespace MatterHackers.MatterControl.DesignTools
this.DrawPath();
}
public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer)
{
return this.GetWorldspaceAabbOfDrawPath();
}
public override bool CanApply => true;
[HideFromEditor]

View file

@ -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();
}
}
}

View file

@ -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;

View file

@ -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();
}
}
}

View file

@ -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)

View file

@ -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();
}
}
}

View file

@ -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;

View file

@ -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<ClipperLib.IntPoint>;
using Polygons = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;
@ -186,5 +187,10 @@ namespace MatterHackers.MatterControl.DesignTools.Operations
{
this.DrawPath();
}
public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer)
{
return this.GetWorldspaceAabbOfDrawPath();
}
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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");

View file

@ -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();
}
}
}

View file

@ -72,6 +72,11 @@ namespace MatterHackers.MatterControl.DesignTools
this.DrawPath();
}
public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer)
{
return this.GetWorldspaceAabbOfDrawPath();
}
/// <summary>
/// This is the actual serialized with that can use expressions
/// </summary>

View file

@ -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

View file

@ -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

View file

@ -173,5 +173,10 @@ namespace MatterHackers.MatterControl.DesignTools
theme.TextColor);
Mesh.PlaceTextureOnFaces(0, texture);
}
public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer)
{
return AxisAlignedBoundingBox.Empty();
}
}
}

View file

@ -199,5 +199,10 @@ namespace MatterHackers.MatterControl.DesignTools
{
this.DrawPath();
}
public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer)
{
return this.GetWorldspaceAabbOfDrawPath();
}
}
}

View file

@ -120,6 +120,11 @@ namespace MatterHackers.MatterControl.DesignTools
}
}
AxisAlignedBoundingBox IObject3DControl.GetWorldspaceAABB()
{
return shape.GetAxisAlignedBoundingBox().NewTransformed(ShapeMatrix());
}
private Matrix4X4 ShapeMatrix()
{
var worldPosition = getPosition();

View file

@ -59,5 +59,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow
world.RenderDebugAABB(e.Graphics2D, child.GetAxisAlignedBoundingBox());
}
}
AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB()
{
// No 3D drawing.
return AxisAlignedBoundingBox.Empty();
}
}
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -43,5 +43,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow
DrawStage DrawStage { get; }
void Draw(GuiWidget sender, DrawEventArgs e, Matrix4X4 itemMaxtrix, WorldView world);
/// <returns>The worldspace AABB of the 3D geometry drawn by Draw.</returns>
AxisAlignedBoundingBox GetWorldspaceAABB();
}
}

View file

@ -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);
/// <returns>The worldspace AABB of the 3D geometry drawn by Draw.</returns>
AxisAlignedBoundingBox GetWorldspaceAABB(IObject3D item, bool isSelected, WorldView world);
}
}

View file

@ -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();
}
}
}

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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<AxisAlignedBoundingBox> aabbs = layer.MakeListOfObjectControlBoundingBoxes();
aabbs.Add(layer.GetPrinterNozzleAABB());
foreach (var box in aabbs)
{
if (box.XSize < 0)
continue;
Vector3[] v = box.GetCorners();
Tuple<Vector3, Vector3>[] lines = new Tuple<Vector3, Vector3>[]{
new Tuple<Vector3, Vector3>(v[0], v[1]),
new Tuple<Vector3, Vector3>(v[2], v[3]),
new Tuple<Vector3, Vector3>(v[0], v[3]),
new Tuple<Vector3, Vector3>(v[1], v[2]),
new Tuple<Vector3, Vector3>(v[4 + 0], v[4 + 1]),
new Tuple<Vector3, Vector3>(v[4 + 2], v[4 + 3]),
new Tuple<Vector3, Vector3>(v[4 + 0], v[4 + 3]),
new Tuple<Vector3, Vector3>(v[4 + 1], v[4 + 2]),
new Tuple<Vector3, Vector3>(v[0], v[4 + 0]),
new Tuple<Vector3, Vector3>(v[1], v[4 + 1]),
new Tuple<Vector3, Vector3>(v[2], v[4 + 2]),
new Tuple<Vector3, Vector3>(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();
}
}
}

View file

@ -71,5 +71,11 @@ namespace MatterHackers.MatterControl.PartPreviewWindow
Object3DControlsLayer.RenderBounds(e, world, bvhIterator);
}
AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB()
{
// No 3D drawing.
return AxisAlignedBoundingBox.Empty();
}
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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);

View file

@ -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);

View file

@ -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
{
/// <summary>
/// This proportion, scaled by the smaller dimension of the viewport, is subtracted from each side of the viewport for fitting.
/// Exposed for testing.
/// </summary>
public const double MarginScale = 0.1;
public enum EPerspectiveFittingAlgorithm
{
/// <summary>
/// Has a margin, but does not use MarginScale.
/// </summary>
TrialAndError,
/// <summary>
/// Fit the camera to the AABB's bounding sphere.
/// Guarantees a centered AABB center, but will not center the screenspace AABB.
/// </summary>
Sphere,
/// <summary>
/// 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.
/// </summary>
CenterOnWorldspaceAABB,
/// <summary>
/// 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.
/// </summary>
CenterOnViewspaceAABB,
/// <summary>
/// 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
/// </summary>
IntersectionOfBoundingPlanesWithApproxCentering,
/// <summary>
/// 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.
/// </summary>
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<PiecewiseSegment> 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<PiecewiseSegment>();
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<PiecewiseSegment> 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<PiecewiseSegment> 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<PiecewiseSegment> piecewiseMax = SweepDescendingMaxY(min, max, ndcLines);
List<PiecewiseSegment> 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<Tetrahedron[]>(() =>
{
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<double, double> 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<double, double>(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<double, double>(depths.Min(), depths.Max());
}
}
return null;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -139,6 +139,12 @@ namespace MatterHackers.MatterControl.PartPreviewWindow
}
}
public override AxisAlignedBoundingBox GetWorldspaceAABB()
{
// No 3D drawing.
return AxisAlignedBoundingBox.Empty();
}
public override void CancelOperation()
{
}

View file

@ -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);
/// <returns>The worldspace AABB of the 3D geometry drawn by Draw.</returns>
AxisAlignedBoundingBox GetWorldspaceAABB();
ITraceable GetTraceable();
}

View file

@ -166,5 +166,8 @@ namespace MatterHackers.MeshVisualizer
return null;
}
/// <returns>The worldspace AABB of the 3D geometry drawn by Draw.</returns>
public abstract AxisAlignedBoundingBox GetWorldspaceAABB();
}
}

View file

@ -80,5 +80,15 @@ namespace MatterHackers.MatterControl.PartPreviewWindow
darkWireframe);
}
}
AxisAlignedBoundingBox IDrawable.GetWorldspaceAABB()
{
if (levelingDataMesh != null)
{
return levelingDataMesh.GetAxisAlignedBoundingBox();
}
return AxisAlignedBoundingBox.Empty();
}
}
}

View file

@ -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<IObject3D> editorDrawItems = new HashSet<IObject3D>();
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<AxisAlignedBoundingBox> MakeListOfObjectControlBoundingBoxes()
{
var selectedItem = scene.SelectedItem;
var aabbs = new List<AxisAlignedBoundingBox>(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();
}
}
}

View file

@ -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<int>((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<TimeAndPosition> motionQueue = new List<TimeAndPosition>();

View file

@ -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<GuiWidget>(),
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<double, double> 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<double, double> ExpandNearAndFarToClippedBounds(Tuple<double, double> nearFar, bool isOrthographic, Plane[] worldspacePlanes, Matrix4X4 worldToViewspace, AxisAlignedBoundingBox bounds)
{
for (int x = 0; x < 2; x++)
Tuple<double, double> 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";

View file

@ -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);
}
}
}

@ -1 +1 @@
Subproject commit 7b9b6716605841d83d1ec8dbc4086e8235245896
Subproject commit e71e8e1fcffba3de1cac98f2b78d0eb5ccc57178

View file

@ -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);
}
}
}

View file

@ -53,6 +53,7 @@
<Compile Include="..\MatterControl.Tests\MatterControl\MatterControlUtilities.cs">
<Link>MatterControlUtilities.cs</Link>
</Compile>
<Compile Include="CameraFittingUtilTests.cs" />
<Compile Include="DesignTools\SheetTests.cs" />
<Compile Include="MatterControlTests.cs" />
<Compile Include="CreateLibraryFolder.cs" />