2021-12-15 18:09:21 -08:00
|
|
|
/*
|
|
|
|
|
Copyright (c) 2021, Lars Brubaker
|
|
|
|
|
All rights reserved.
|
|
|
|
|
|
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
|
|
|
modification, are permitted provided that the following conditions are met:
|
|
|
|
|
|
|
|
|
|
1. Redistributions of source code must retain the above copyright notice, this
|
|
|
|
|
list of conditions and the following disclaimer.
|
|
|
|
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
|
this list of conditions and the following disclaimer in the documentation
|
|
|
|
|
and/or other materials provided with the distribution.
|
|
|
|
|
|
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
|
|
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
|
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
|
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
|
|
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
|
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
|
|
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
|
|
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
|
|
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
using MatterHackers.Agg;
|
|
|
|
|
using MatterHackers.Agg.UI;
|
2021-04-30 15:06:14 -07:00
|
|
|
using MatterHackers.Agg.VertexSource;
|
2021-04-15 00:35:27 +02:00
|
|
|
using MatterHackers.RayTracer;
|
|
|
|
|
using MatterHackers.VectorMath;
|
|
|
|
|
using MatterHackers.VectorMath.TrackBall;
|
2021-04-30 17:36:50 -07:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2022-03-02 00:52:04 +00:00
|
|
|
using System.Linq;
|
2021-04-15 00:35:27 +02:00
|
|
|
|
|
|
|
|
namespace MatterHackers.MatterControl.PartPreviewWindow
|
|
|
|
|
{
|
2021-12-15 18:09:21 -08:00
|
|
|
public class TrackballTumbleWidgetExtended : GuiWidget
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
private const double PerspectiveMinZoomDist = 3;
|
|
|
|
|
private const double PerspectiveMaxZoomDist = 2300;
|
2022-03-04 23:09:18 +00:00
|
|
|
|
|
|
|
|
// Currently 2.7614237491539679
|
|
|
|
|
private double OrthographicMinZoomWorldspaceHeight
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
// Get the Z plane height at the perspective limit.
|
|
|
|
|
// By coincidence, these are currently about the same, with byPerspectiveZoomLimit being slightly less at 2.4852813742385704.
|
|
|
|
|
double byPerspectiveZoomLimit = WorldView.CalcPerspectiveHeight(PerspectiveMinZoomDist, WorldView.DefaultPerspectiveVFOVDegrees);
|
|
|
|
|
double byWorldViewLimit = WorldView.OrthographicProjectionMinimumHeight * Vector3.UnitY.TransformVector(this.world.InverseModelviewMatrix).Length;
|
|
|
|
|
return Math.Max(byPerspectiveZoomLimit, byWorldViewLimit);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Currently 1905.3823869162372
|
|
|
|
|
private double OrthographicMaxZoomWorldspaceHeight => WorldView.CalcPerspectiveHeight(PerspectiveMaxZoomDist, WorldView.DefaultPerspectiveVFOVDegrees);
|
|
|
|
|
|
|
|
|
|
// When switching the projection from perspective to orthographic, ensure this minimum height.
|
|
|
|
|
// Will tend to be used when fully zoomed into the hit plane, and then the ref position indicator will appear to drift during the animation.
|
|
|
|
|
// The resulting projection might be undesired, but at least it would be non-zero.
|
|
|
|
|
private double PerspectiveToOrthographicMinViewspaceHeight => WorldView.OrthographicProjectionMinimumHeight;
|
2022-03-02 00:52:04 +00:00
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
public NearFarAction GetNearFar;
|
|
|
|
|
private readonly MotionQueue motionQueue = new MotionQueue();
|
|
|
|
|
private readonly GuiWidget sourceWidget;
|
|
|
|
|
private readonly int updatesPerSecond = 30;
|
2021-04-30 17:36:50 -07:00
|
|
|
private double _centerOffsetX = 0;
|
|
|
|
|
private Vector2 currentVelocityPerMs = new Vector2();
|
|
|
|
|
private PlaneShape hitPlane;
|
2021-04-15 00:35:27 +02:00
|
|
|
private bool isRotating = false;
|
2021-04-15 09:53:43 -07:00
|
|
|
private Vector2 lastScaleMousePosition = Vector2.Zero;
|
2021-05-04 17:17:55 -07:00
|
|
|
private Vector2 rotationStartPosition = Vector2.Zero;
|
2021-04-30 15:06:14 -07:00
|
|
|
private Vector3 mouseDownWorldPosition;
|
2021-05-21 12:48:03 -07:00
|
|
|
private readonly Object3DControlsLayer Object3DControlLayer;
|
2021-04-30 17:36:50 -07:00
|
|
|
private RunningInterval runningInterval;
|
2021-05-21 12:48:03 -07:00
|
|
|
private readonly ThemeConfig theme;
|
|
|
|
|
private readonly WorldView world;
|
2021-05-04 17:17:55 -07:00
|
|
|
private Vector2 mouseDown;
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2021-04-30 15:06:14 -07:00
|
|
|
public TrackballTumbleWidgetExtended(WorldView world, GuiWidget sourceWidget, Object3DControlsLayer Object3DControlLayer, ThemeConfig theme)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
|
|
|
|
AnchorAll();
|
|
|
|
|
TrackBallController = new TrackBallController(world);
|
2021-04-30 15:06:14 -07:00
|
|
|
this.theme = theme;
|
2021-04-15 00:35:27 +02:00
|
|
|
this.world = world;
|
|
|
|
|
this.sourceWidget = sourceWidget;
|
|
|
|
|
this.Object3DControlLayer = Object3DControlLayer;
|
2022-03-02 00:52:04 +00:00
|
|
|
this.PerspectiveMode = !this.world.IsOrthographic;
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
public delegate void NearFarAction(WorldView world, out double zNear, out double zFar);
|
2021-04-30 17:36:50 -07:00
|
|
|
|
|
|
|
|
public double CenterOffsetX
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _centerOffsetX;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_centerOffsetX = value;
|
|
|
|
|
RecalculateProjection();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public TrackBallTransformType CurrentTrackingType { get; set; } = TrackBallTransformType.None;
|
|
|
|
|
public TrackBallController TrackBallController { get; }
|
|
|
|
|
public TrackBallTransformType TransformState { get; set; }
|
2021-05-04 17:17:55 -07:00
|
|
|
public double ZoomDelta { get; set; } = 0.2f;
|
2022-03-02 00:52:04 +00:00
|
|
|
public double OrthographicZoomScalingFactor { get; set; } = 1.2f;
|
2021-05-26 07:44:33 -07:00
|
|
|
public bool TurntableEnabled { get; set; }
|
2022-03-02 00:52:04 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2021-04-30 17:36:50 -07:00
|
|
|
|
|
|
|
|
public void DoRotateAroundOrigin(Vector2 mousePosition)
|
|
|
|
|
{
|
|
|
|
|
if (isRotating)
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-05-26 11:38:44 -07:00
|
|
|
Quaternion activeRotationQuaternion;
|
|
|
|
|
if (TurntableEnabled)
|
|
|
|
|
{
|
|
|
|
|
var delta = mousePosition - rotationStartPosition;
|
2021-05-26 12:13:53 -07:00
|
|
|
// scale it to device units
|
2021-05-26 14:39:19 -07:00
|
|
|
delta /= TrackBallController.TrackBallRadius / 2;
|
|
|
|
|
var zRotation = Matrix4X4.CreateFromAxisAngle(Vector3.UnitZ.Transform(world.RotationMatrix), delta.X);
|
|
|
|
|
var screenXRotation = Matrix4X4.CreateFromAxisAngle(Vector3.UnitX, -delta.Y);
|
2021-05-26 11:38:44 -07:00
|
|
|
activeRotationQuaternion = new Quaternion(zRotation * screenXRotation);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
activeRotationQuaternion = TrackBallController.GetRotationForMove(TrackBallController.ScreenCenter,
|
|
|
|
|
TrackBallController.TrackBallRadius,
|
|
|
|
|
rotationStartPosition,
|
|
|
|
|
mousePosition,
|
|
|
|
|
false);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
rotationStartPosition = mousePosition;
|
2021-04-30 17:36:50 -07:00
|
|
|
|
2021-05-21 12:48:03 -07:00
|
|
|
world.RotateAroundPosition(mouseDownWorldPosition, activeRotationQuaternion);
|
2021-04-30 17:36:50 -07:00
|
|
|
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void EndRotateAroundOrigin()
|
|
|
|
|
{
|
|
|
|
|
if (isRotating)
|
|
|
|
|
{
|
|
|
|
|
isRotating = false;
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnDraw(Graphics2D graphics2D)
|
|
|
|
|
{
|
2022-03-04 20:10:03 +00:00
|
|
|
void drawCircle(Vector3 worldspacePosition)
|
2021-04-30 17:36:50 -07:00
|
|
|
{
|
2022-03-04 20:10:03 +00:00
|
|
|
var circle = new Ellipse(world.GetScreenPosition(worldspacePosition), 8 * DeviceScale);
|
2022-03-02 00:52:04 +00:00
|
|
|
graphics2D.Render(new Stroke(circle, 2 * DeviceScale), theme.PrimaryAccentColor);
|
|
|
|
|
graphics2D.Render(new Stroke(new Stroke(circle, 4 * DeviceScale), DeviceScale), theme.TextColor.WithAlpha(128));
|
2021-04-30 17:36:50 -07:00
|
|
|
}
|
|
|
|
|
|
2022-03-04 20:10:03 +00:00
|
|
|
if (TrackBallController.CurrentTrackingType == TrackBallTransformType.None)
|
|
|
|
|
{
|
|
|
|
|
switch (TransformState)
|
|
|
|
|
{
|
|
|
|
|
case TrackBallTransformType.Translation:
|
|
|
|
|
case TrackBallTransformType.Rotation:
|
|
|
|
|
drawCircle(mouseDownWorldPosition);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isSwitchingProjectionMode = _perspectiveModeSwitchFinishAnimation != null;
|
|
|
|
|
|
|
|
|
|
if (isSwitchingProjectionMode)
|
|
|
|
|
{
|
|
|
|
|
drawCircle(GetWorldRefPositionForProjectionSwitch());
|
|
|
|
|
}
|
2022-03-02 00:52:04 +00:00
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
base.OnDraw(graphics2D);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnDraw3D()
|
|
|
|
|
{
|
|
|
|
|
if (hitPlane != null)
|
|
|
|
|
{
|
2021-05-02 22:12:57 -07:00
|
|
|
//world.RenderPlane(hitPlane.Plane, Color.Red, true, 50, 3);
|
|
|
|
|
//world.RenderPlane(mouseDownWorldPosition, hitPlane.Plane.Normal, Color.Red, true, 50, 3);
|
2021-05-04 17:17:55 -07:00
|
|
|
|
|
|
|
|
//world.RenderAxis(mouseDownWorldPosition, Matrix4X4.Identity, 100, 1);
|
2021-04-30 17:36:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
public override void OnMouseDown(MouseEventArgs mouseEvent)
|
|
|
|
|
{
|
|
|
|
|
base.OnMouseDown(mouseEvent);
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
mouseDown = mouseEvent.Position;
|
|
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
if (MouseCaptured)
|
|
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
|
2021-04-30 13:56:59 -07:00
|
|
|
CalculateMouseDownPostionAndPlane(mouseEvent.Position);
|
|
|
|
|
|
2021-04-15 09:53:43 -07:00
|
|
|
if (mouseEvent.Button == MouseButtons.Left)
|
|
|
|
|
{
|
|
|
|
|
if (TrackBallController.CurrentTrackingType == TrackBallTransformType.None)
|
|
|
|
|
{
|
|
|
|
|
switch (TransformState)
|
|
|
|
|
{
|
|
|
|
|
case TrackBallTransformType.Rotation:
|
|
|
|
|
CurrentTrackingType = TrackBallTransformType.Rotation;
|
2021-05-03 17:22:48 -07:00
|
|
|
StartRotateAroundOrigin(mouseEvent.Position);
|
2021-04-15 09:53:43 -07:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TrackBallTransformType.Translation:
|
|
|
|
|
CurrentTrackingType = TrackBallTransformType.Translation;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TrackBallTransformType.Scale:
|
|
|
|
|
CurrentTrackingType = TrackBallTransformType.Scale;
|
2021-05-03 17:22:48 -07:00
|
|
|
lastScaleMousePosition = mouseEvent.Position;
|
2021-04-15 09:53:43 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (mouseEvent.Button == MouseButtons.Middle)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2021-04-16 12:59:43 -07:00
|
|
|
if (CurrentTrackingType == TrackBallTransformType.None)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
|
|
|
|
CurrentTrackingType = TrackBallTransformType.Translation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (mouseEvent.Button == MouseButtons.Right)
|
|
|
|
|
{
|
2021-04-16 12:59:43 -07:00
|
|
|
if (CurrentTrackingType == TrackBallTransformType.None)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
|
|
|
|
CurrentTrackingType = TrackBallTransformType.Rotation;
|
2021-05-03 17:22:48 -07:00
|
|
|
StartRotateAroundOrigin(mouseEvent.Position);
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnMouseMove(MouseEventArgs mouseEvent)
|
|
|
|
|
{
|
|
|
|
|
base.OnMouseMove(mouseEvent);
|
|
|
|
|
|
|
|
|
|
if (CurrentTrackingType == TrackBallTransformType.Rotation)
|
|
|
|
|
{
|
2021-05-03 17:22:48 -07:00
|
|
|
motionQueue.AddMoveToMotionQueue(mouseEvent.Position, UiThread.CurrentTimerMs);
|
|
|
|
|
DoRotateAroundOrigin(mouseEvent.Position);
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
2021-04-16 12:59:43 -07:00
|
|
|
else if (CurrentTrackingType == TrackBallTransformType.Translation)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2021-05-03 17:22:48 -07:00
|
|
|
Translate(mouseEvent.Position);
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
2021-04-16 12:59:43 -07:00
|
|
|
else if (CurrentTrackingType == TrackBallTransformType.Scale)
|
2021-04-15 09:53:43 -07:00
|
|
|
{
|
2021-05-29 15:15:35 -07:00
|
|
|
Vector2 mouseDelta = (mouseEvent.Position - lastScaleMousePosition) / GuiWidget.DeviceScale;
|
|
|
|
|
double zoomDelta = mouseDelta.Y < 0 ? .01 : -.01;
|
2021-04-15 09:53:43 -07:00
|
|
|
|
2021-05-29 15:15:35 -07:00
|
|
|
for(int i=0; i<Math.Abs(mouseDelta.Y); i++)
|
2021-05-04 17:17:55 -07:00
|
|
|
{
|
2021-05-29 15:15:35 -07:00
|
|
|
ZoomToWorldPosition(mouseDownWorldPosition, zoomDelta);
|
2021-05-04 17:17:55 -07:00
|
|
|
}
|
2021-05-03 17:22:48 -07:00
|
|
|
lastScaleMousePosition = mouseEvent.Position;
|
2021-04-15 09:53:43 -07:00
|
|
|
}
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnMouseUp(MouseEventArgs mouseEvent)
|
|
|
|
|
{
|
|
|
|
|
if (CurrentTrackingType != TrackBallTransformType.None)
|
|
|
|
|
{
|
|
|
|
|
if (CurrentTrackingType == TrackBallTransformType.Rotation)
|
|
|
|
|
{
|
|
|
|
|
EndRotateAroundOrigin();
|
|
|
|
|
|
|
|
|
|
// try and preserve some of the velocity
|
|
|
|
|
motionQueue.AddMoveToMotionQueue(mouseEvent.Position, UiThread.CurrentTimerMs);
|
|
|
|
|
if (!Keyboard.IsKeyDown(Keys.ShiftKey))
|
|
|
|
|
{
|
|
|
|
|
currentVelocityPerMs = motionQueue.GetVelocityPixelsPerMs();
|
|
|
|
|
if (currentVelocityPerMs.LengthSquared > 0)
|
|
|
|
|
{
|
|
|
|
|
Vector2 center = LocalBounds.Center;
|
|
|
|
|
StartRotateAroundOrigin(center);
|
|
|
|
|
runningInterval = UiThread.SetInterval(ApplyVelocity, 1.0 / updatesPerSecond);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CurrentTrackingType = TrackBallTransformType.None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
base.OnMouseUp(mouseEvent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnMouseWheel(MouseEventArgs mouseEvent)
|
|
|
|
|
{
|
2021-04-19 07:38:37 -07:00
|
|
|
if (this.ContainsFirstUnderMouseRecursive())
|
|
|
|
|
{
|
2021-05-04 17:17:55 -07:00
|
|
|
var mousePosition = mouseEvent.Position;
|
|
|
|
|
var zoomDelta = mouseEvent.WheelDelta > 0 ? -ZoomDelta : ZoomDelta;
|
|
|
|
|
ZoomToMousePosition(mousePosition, zoomDelta);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-30 15:06:14 -07:00
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
private void ZoomToMousePosition(Vector2 mousePosition, double zoomDelta)
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
var rayAtScreenCenter = world.GetRayForLocalBounds(new Vector2(Width / 2, Height / 2));
|
|
|
|
|
var rayAtMousePosition = world.GetRayForLocalBounds(mousePosition);
|
|
|
|
|
IntersectInfo intersectionInfo = Object3DControlLayer.Scene.GetBVHData().GetClosestIntersection(rayAtMousePosition);
|
|
|
|
|
if (intersectionInfo != null)
|
|
|
|
|
{
|
|
|
|
|
// we hit an object in the scene set the position to that
|
|
|
|
|
hitPlane = new PlaneShape(new Plane(rayAtScreenCenter.directionNormal, mouseDownWorldPosition), null);
|
|
|
|
|
ZoomToWorldPosition(intersectionInfo.HitPosition, zoomDelta);
|
|
|
|
|
mouseDownWorldPosition = intersectionInfo.HitPosition;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 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);
|
2022-03-02 00:52:04 +00:00
|
|
|
intersectionInfo = hitPlane.GetClosestIntersectionWithinRayDistanceRange(rayAtMousePosition);
|
2021-05-04 17:17:55 -07:00
|
|
|
if (intersectionInfo != null)
|
2021-04-30 15:06:14 -07:00
|
|
|
{
|
2021-05-04 17:17:55 -07:00
|
|
|
ZoomToWorldPosition(intersectionInfo.HitPosition, zoomDelta);
|
|
|
|
|
mouseDownWorldPosition = intersectionInfo.HitPosition;
|
|
|
|
|
}
|
2021-04-19 07:38:37 -07:00
|
|
|
}
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
public void RecalculateProjection()
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2021-04-30 17:36:50 -07:00
|
|
|
double trackingRadius = Math.Min(Width * .45, Height * .45);
|
2022-03-02 00:52:04 +00:00
|
|
|
|
|
|
|
|
// TODO: Should probably be `Width / 2`, but currently has no effect?
|
2021-04-30 17:36:50 -07:00
|
|
|
TrackBallController.ScreenCenter = new Vector2(Width / 2 - CenterOffsetX, Height / 2);
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
TrackBallController.TrackBallRadius = trackingRadius;
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
double zNear = WorldView.DefaultNearZ;
|
|
|
|
|
double zFar = WorldView.DefaultFarZ;
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
Vector2 newViewportSize = new Vector2(Math.Max(1, sourceWidget.LocalBounds.Width), Math.Max(1, sourceWidget.LocalBounds.Height));
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
// Update the projection parameters for GetNearFar.
|
|
|
|
|
// NOTE: PerspectiveMode != this.world.IsOrthographic due to transition animations.
|
|
|
|
|
if (this.world.IsOrthographic)
|
|
|
|
|
{
|
|
|
|
|
this.world.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(newViewportSize.X, newViewportSize.Y, CenterOffsetX, this.world.NearPlaneHeightInViewspace, zNear, zFar);
|
|
|
|
|
}
|
|
|
|
|
else
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
this.world.CalculatePerspectiveMatrixOffCenter(newViewportSize.X, newViewportSize.Y, CenterOffsetX, zNear, zFar, this.world.VFovDegrees);
|
|
|
|
|
}
|
2021-05-27 15:18:51 -07:00
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
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);
|
2021-04-30 17:36:50 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
this.world.CalculatePerspectiveMatrixOffCenter(newViewportSize.X, newViewportSize.Y, CenterOffsetX, zNear, zFar, this.world.VFovDegrees);
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 14:20:28 -07:00
|
|
|
public void SetRotationCenter(Vector3 worldPosition)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2021-04-30 17:36:50 -07:00
|
|
|
ZeroVelocity();
|
2021-05-22 14:20:28 -07:00
|
|
|
mouseDownWorldPosition = worldPosition;
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetRotationWithDisplacement(Quaternion rotationQ)
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-05-21 12:48:03 -07:00
|
|
|
if (isRotating)
|
|
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
}
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2021-05-21 12:48:03 -07:00
|
|
|
world.SetRotationHoldPosition(mouseDownWorldPosition, rotationQ);
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
public void StartRotateAroundOrigin(Vector2 mousePosition)
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
if (isRotating)
|
|
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
rotationStartPosition = mousePosition;
|
2021-05-03 17:22:48 -07:00
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
isRotating = true;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
public void Translate(Vector2 position)
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-04-16 12:59:43 -07:00
|
|
|
if (isRotating)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-13 18:12:22 -07:00
|
|
|
if (hitPlane != null)
|
|
|
|
|
{
|
|
|
|
|
var rayAtPosition = world.GetRayForLocalBounds(position);
|
2022-03-02 00:52:04 +00:00
|
|
|
var hitAtPosition = hitPlane.GetClosestIntersectionWithinRayDistanceRange(rayAtPosition);
|
2021-05-04 17:17:55 -07:00
|
|
|
|
2021-05-13 18:12:22 -07:00
|
|
|
if (hitAtPosition != null)
|
|
|
|
|
{
|
|
|
|
|
var offset = hitAtPosition.HitPosition - mouseDownWorldPosition;
|
|
|
|
|
world.Translate(offset);
|
2021-04-15 00:35:27 +02:00
|
|
|
|
2021-05-13 18:12:22 -07:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-15 00:35:27 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
public void ZeroVelocity()
|
|
|
|
|
{
|
|
|
|
|
motionQueue.Clear();
|
|
|
|
|
currentVelocityPerMs = Vector2.Zero;
|
|
|
|
|
if (runningInterval != null)
|
|
|
|
|
{
|
|
|
|
|
UiThread.ClearInterval(runningInterval);
|
|
|
|
|
}
|
|
|
|
|
EndRotateAroundOrigin();
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
2021-04-30 17:36:50 -07:00
|
|
|
}
|
|
|
|
|
|
2021-04-30 17:06:15 -07:00
|
|
|
public void ZoomToWorldPosition(Vector3 worldPosition, double zoomDelta)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-04-16 12:59:43 -07:00
|
|
|
if (isRotating)
|
2021-04-15 00:35:27 +02:00
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculate the vector between the camera and the intersection position and move the camera along it by ZoomDelta, then set it's direction
|
2021-05-29 15:15:35 -07:00
|
|
|
var delta = worldPosition - world.EyePosition;
|
2022-03-02 00:52:04 +00:00
|
|
|
|
|
|
|
|
if (this.world.IsOrthographic)
|
2021-05-29 15:15:35 -07:00
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
bool isZoomIn = zoomDelta < 0;
|
|
|
|
|
double scaleFactor = isZoomIn ? 1 / OrthographicZoomScalingFactor : OrthographicZoomScalingFactor;
|
|
|
|
|
double newViewspaceHeight = this.world.NearPlaneHeightInViewspace * scaleFactor;
|
2022-03-04 20:10:03 +00:00
|
|
|
double newWorldspaceHeight = Vector3.UnitY.TransformVector(this.world.InverseModelviewMatrix).Length * newViewspaceHeight;
|
2022-03-02 00:52:04 +00:00
|
|
|
|
|
|
|
|
if (isZoomIn
|
2022-03-04 20:10:03 +00:00
|
|
|
? newWorldspaceHeight < OrthographicMinZoomWorldspaceHeight
|
|
|
|
|
: newWorldspaceHeight > OrthographicMaxZoomWorldspaceHeight)
|
2022-03-02 00:52:04 +00:00
|
|
|
{
|
|
|
|
|
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);
|
2021-05-29 15:15:35 -07:00
|
|
|
}
|
2022-03-02 00:52:04 +00:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var deltaLength = delta.Length;
|
|
|
|
|
|
|
|
|
|
if ((deltaLength < PerspectiveMinZoomDist && zoomDelta < 0)
|
|
|
|
|
|| (deltaLength > PerspectiveMaxZoomDist && zoomDelta > 0))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-29 15:15:35 -07:00
|
|
|
var zoomVec = delta * zoomDelta;
|
2021-04-15 00:35:27 +02:00
|
|
|
world.Translate(zoomVec);
|
|
|
|
|
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
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.
|
2022-12-28 14:14:38 -08:00
|
|
|
CameraFittingUtil.FitResult fitResult = CameraFittingUtil.ComputeOrthographicCameraFit(world, CenterOffsetX, 0, 1, box);
|
2022-03-02 00:52:04 +00:00
|
|
|
|
|
|
|
|
WorldView tempWorld = new WorldView(world.Width, world.Height);
|
2022-12-28 14:14:38 -08:00
|
|
|
tempWorld.CalculateOrthogrphicMatrixOffCenterWithViewspaceHeight(world.Width, world.Height, CenterOffsetX, fitResult.OrthographicViewspaceHeight, 0, 1);
|
2022-03-02 00:52:04 +00:00
|
|
|
double endViewspaceHeight = tempWorld.NearPlaneHeightInViewspace;
|
|
|
|
|
double startViewspaceHeight = world.NearPlaneHeightInViewspace;
|
|
|
|
|
|
|
|
|
|
AnimateOrthographicTranslationAndHeight(
|
|
|
|
|
world.EyePosition, startViewspaceHeight,
|
2022-12-28 14:14:38 -08:00
|
|
|
fitResult.CameraPosition, endViewspaceHeight
|
2022-03-02 00:52:04 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2022-12-28 14:14:38 -08:00
|
|
|
CameraFittingUtil.FitResult fitResult = CameraFittingUtil.ComputePerspectiveCameraFit(world, CenterOffsetX, box);
|
|
|
|
|
AnimateTranslation(fitResult.CameraPosition, world.EyePosition);
|
2022-03-02 00:52:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
private void ApplyVelocity()
|
|
|
|
|
{
|
2021-04-16 12:59:43 -07:00
|
|
|
if (isRotating)
|
|
|
|
|
{
|
2021-04-15 00:35:27 +02:00
|
|
|
if (HasBeenClosed || currentVelocityPerMs.LengthSquared <= 0)
|
|
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double msPerUpdate = 1000.0 / updatesPerSecond;
|
|
|
|
|
if (currentVelocityPerMs.LengthSquared > 0)
|
|
|
|
|
{
|
|
|
|
|
if (CurrentTrackingType == TrackBallTransformType.None)
|
|
|
|
|
{
|
|
|
|
|
Vector2 center = LocalBounds.Center;
|
2021-05-04 17:17:55 -07:00
|
|
|
rotationStartPosition = center;
|
2021-04-15 00:35:27 +02:00
|
|
|
DoRotateAroundOrigin(center + currentVelocityPerMs * msPerUpdate);
|
|
|
|
|
Invalidate();
|
|
|
|
|
|
|
|
|
|
currentVelocityPerMs *= .85;
|
|
|
|
|
if (currentVelocityPerMs.LengthSquared < .01 / msPerUpdate)
|
|
|
|
|
{
|
|
|
|
|
ZeroVelocity();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
private Vector3 GetWorldRefPositionForProjectionSwitch()
|
|
|
|
|
{
|
|
|
|
|
return mouseDownWorldPosition;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-30 17:36:50 -07:00
|
|
|
private void CalculateMouseDownPostionAndPlane(Vector2 mousePosition)
|
|
|
|
|
{
|
2022-03-02 00:52:04 +00:00
|
|
|
FinishProjectionSwitch();
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
var rayAtMousePosition = world.GetRayForLocalBounds(mousePosition);
|
2022-04-26 13:40:16 -07:00
|
|
|
|
|
|
|
|
// TODO:
|
|
|
|
|
// Check if we are in a GCode View
|
|
|
|
|
var showingGCode = false;
|
|
|
|
|
if (showingGCode)
|
|
|
|
|
{
|
|
|
|
|
// find the layer height that we are currenly showing
|
|
|
|
|
// create a plane at that height
|
|
|
|
|
// check if the ray intersects this plane
|
|
|
|
|
// if we are above the plane set our origin to this intersection
|
|
|
|
|
// if we are below the plane set it as a limit to the cast distance distance
|
|
|
|
|
|
|
|
|
|
// Consideration: Think about what to do if we would be hitting the top of the part that is the layer height plane.
|
|
|
|
|
// The issue is that there is no mesh geometry at that height but the user will see gcode that they might think
|
|
|
|
|
// they should be able to click on.
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
var intersectionInfo = Object3DControlLayer.Scene.GetBVHData().GetClosestIntersection(rayAtMousePosition);
|
2021-04-30 17:36:50 -07:00
|
|
|
var rayAtScreenCenter = world.GetRayForLocalBounds(new Vector2(Width / 2, Height / 2));
|
2021-05-04 17:17:55 -07:00
|
|
|
if (intersectionInfo != null)
|
2021-04-30 17:36:50 -07:00
|
|
|
{
|
2021-05-04 17:17:55 -07:00
|
|
|
// we hit an object in the scene set the position to that
|
|
|
|
|
mouseDownWorldPosition = intersectionInfo.HitPosition;
|
|
|
|
|
hitPlane = new PlaneShape(new Plane(rayAtScreenCenter.directionNormal, mouseDownWorldPosition), null);
|
2021-04-30 17:36:50 -07:00
|
|
|
}
|
2021-05-04 17:17:55 -07:00
|
|
|
else
|
2021-04-30 17:36:50 -07:00
|
|
|
{
|
2021-05-04 17:17:55 -07:00
|
|
|
// 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);
|
2022-03-02 00:52:04 +00:00
|
|
|
intersectionInfo = hitPlane.GetClosestIntersectionWithinRayDistanceRange(rayAtMousePosition);
|
|
|
|
|
|
2021-05-04 17:17:55 -07:00
|
|
|
if (intersectionInfo != null)
|
|
|
|
|
{
|
|
|
|
|
mouseDownWorldPosition = intersectionInfo.HitPosition;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
int a = 0;
|
|
|
|
|
}
|
2021-04-30 17:36:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-24 07:36:04 -07:00
|
|
|
public void AnimateRotation(Matrix4X4 newRotation, Action after = null)
|
2021-05-22 14:20:28 -07:00
|
|
|
{
|
|
|
|
|
var rotationStart = new Quaternion(world.RotationMatrix);
|
|
|
|
|
var rotationEnd = new Quaternion(newRotation);
|
2021-05-23 06:30:56 -07:00
|
|
|
ZeroVelocity();
|
|
|
|
|
var updates = 10;
|
|
|
|
|
Animation.Run(this, .25, updates, (update) =>
|
2021-05-22 14:20:28 -07:00
|
|
|
{
|
2021-05-23 06:30:56 -07:00
|
|
|
var current = Quaternion.Slerp(rotationStart, rotationEnd, update / (double)updates);
|
|
|
|
|
this.SetRotationWithDisplacement(current);
|
2021-05-24 07:36:04 -07:00
|
|
|
}, after);
|
2021-05-22 14:20:28 -07:00
|
|
|
}
|
|
|
|
|
|
2021-05-24 07:36:04 -07:00
|
|
|
public void AnimateTranslation(Vector3 worldStart, Vector3 worldEnd, Action after = null)
|
2021-05-22 22:24:12 -07:00
|
|
|
{
|
2021-05-24 07:36:04 -07:00
|
|
|
var delta = worldEnd - worldStart;
|
2021-05-22 22:24:12 -07:00
|
|
|
|
2021-05-23 06:30:56 -07:00
|
|
|
ZeroVelocity();
|
|
|
|
|
Animation.Run(this, .25, 10, (update) =>
|
|
|
|
|
{
|
|
|
|
|
world.Translate(delta * .1);
|
2021-05-24 07:36:04 -07:00
|
|
|
}, after);
|
2021-05-22 22:24:12 -07:00
|
|
|
}
|
|
|
|
|
|
2022-03-02 00:52:04 +00:00
|
|
|
// 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));
|
2022-03-04 23:09:18 +00:00
|
|
|
|
|
|
|
|
double refViewspaceZ = viewspaceRefPosition.Z;
|
|
|
|
|
|
|
|
|
|
// Ensure a minimum when going from perspective (in case the camera is zoomed all the way into the hit plane).
|
|
|
|
|
if (toOrthographic)
|
|
|
|
|
{
|
|
|
|
|
double minViewspaceHeight = PerspectiveToOrthographicMinViewspaceHeight;
|
|
|
|
|
if (refPlaneHeightInViewspace < minViewspaceHeight)
|
|
|
|
|
{
|
|
|
|
|
// If this happens, the ref position indicator will appear to drift during the animation.
|
|
|
|
|
refPlaneHeightInViewspace = minViewspaceHeight;
|
|
|
|
|
refViewspaceZ = -WorldView.CalcPerspectiveDistance(minViewspaceHeight, this.world.VFovDegrees);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-03-02 00:52:04 +00:00
|
|
|
|
2022-03-04 23:31:57 +00:00
|
|
|
double refFOVDegrees =
|
2022-03-02 00:52:04 +00:00
|
|
|
toOrthographic
|
|
|
|
|
? this.world.VFovDegrees // start FOV
|
|
|
|
|
: WorldView.DefaultPerspectiveVFOVDegrees // end FOV
|
2022-03-04 23:31:57 +00:00
|
|
|
;
|
2022-03-02 00:52:04 +00:00
|
|
|
|
|
|
|
|
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;
|
2022-03-04 23:31:57 +00:00
|
|
|
double fov = toOrthographic ? refFOVDegrees * (1 - t) : refFOVDegrees * t;
|
2022-03-02 00:52:04 +00:00
|
|
|
|
2022-03-04 23:31:57 +00:00
|
|
|
double dist = WorldView.CalcPerspectiveDistance(refPlaneHeightInViewspace, fov);
|
2022-03-04 23:09:18 +00:00
|
|
|
double eyeZ = refViewspaceZ + dist;
|
2022-03-02 00:52:04 +00:00
|
|
|
|
|
|
|
|
Vector3 viewspaceEyePosition = new Vector3(0, 0, eyeZ);
|
|
|
|
|
|
|
|
|
|
//System.Diagnostics.Trace.WriteLine("{0} {1} {2}".FormatWith(fovDegrees, dist, eyeZ));
|
|
|
|
|
|
2022-03-04 23:31:57 +00:00
|
|
|
world.CalculatePerspectiveMatrixOffCenter(world.Width, world.Height, CenterOffsetX, WorldView.DefaultNearZ, WorldView.DefaultFarZ, fov);
|
2022-03-02 00:52:04 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 00:35:27 +02:00
|
|
|
internal class MotionQueue
|
|
|
|
|
{
|
|
|
|
|
private readonly List<TimeAndPosition> motionQueue = new List<TimeAndPosition>();
|
|
|
|
|
|
|
|
|
|
internal void AddMoveToMotionQueue(Vector2 position, long timeMs)
|
|
|
|
|
{
|
|
|
|
|
if (motionQueue.Count > 4)
|
|
|
|
|
{
|
|
|
|
|
// take off the last one
|
|
|
|
|
motionQueue.RemoveAt(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
motionQueue.Add(new TimeAndPosition(position, timeMs));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void Clear()
|
|
|
|
|
{
|
|
|
|
|
motionQueue.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal Vector2 GetVelocityPixelsPerMs()
|
|
|
|
|
{
|
|
|
|
|
if (motionQueue.Count > 1)
|
|
|
|
|
{
|
|
|
|
|
// Get all the movement that is less 100 ms from the last time (the mouse up)
|
|
|
|
|
TimeAndPosition lastTime = motionQueue[motionQueue.Count - 1];
|
|
|
|
|
int firstTimeIndex = motionQueue.Count - 1;
|
|
|
|
|
while (firstTimeIndex > 0 && motionQueue[firstTimeIndex - 1].timeMs + 100 > lastTime.timeMs)
|
|
|
|
|
{
|
|
|
|
|
firstTimeIndex--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TimeAndPosition firstTime = motionQueue[firstTimeIndex];
|
|
|
|
|
|
|
|
|
|
double milliseconds = lastTime.timeMs - firstTime.timeMs;
|
|
|
|
|
if (milliseconds > 0)
|
|
|
|
|
{
|
|
|
|
|
Vector2 pixels = lastTime.position - firstTime.position;
|
|
|
|
|
Vector2 pixelsPerSecond = pixels / milliseconds;
|
|
|
|
|
|
|
|
|
|
return pixelsPerSecond;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Vector2.Zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal struct TimeAndPosition
|
|
|
|
|
{
|
|
|
|
|
internal Vector2 position;
|
|
|
|
|
|
|
|
|
|
internal long timeMs;
|
|
|
|
|
|
|
|
|
|
internal TimeAndPosition(Vector2 position, long timeMs)
|
|
|
|
|
{
|
|
|
|
|
this.timeMs = timeMs;
|
|
|
|
|
this.position = position;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|