17 Mar 2024

Camera mapping between APS viewer and Revit - Part I, restore viewer camera on Revit

About five years ago, we shared a blog post about how to Map Forge Viewer Camera back to Revit. However, the approach in the blog post has some limitations and assumptions.

  • Assume the model in the APS viewer is offset with the default approach by the global offset, which is the center of the model's bounding box.
  • Manual steps are required to change Revit view configurations, and only perspective views are supported.

 

Now we are pleased to share the more automatic way of supporting both perspective and orthographic cameras. However, here is a safe harbor and disclaimer: "As we mentioned in the previous blog post, please be aware that the camera parameters in APS Viewer and Revit are different, so the mapping results would not be perfectly matched."

 

Ok, let's start our conversion steps now. The first step is similar to our discussion in the previous blog post. We get the current camera state using Viewer3D#getState({ viewport: true }). But here, we map the camera position and target from viewer coordinate space to Revit space by multiplying the transform we get from `model.getInverseModelToViewerTransform()` in the viewer instead.

const state = viewer.getState({ viewport: true });
const model = viewer.getAllModels()[0];
const invTransform = model.getInverseModelToViewerTransform();

const currentTarget = new THREE.Vector3().fromArray( state.viewport.target );
// {x: -14.770469665527344, y: 36.571967124938965, z: -1.212925910949707}

const currentPosition = new THREE.Vector3().fromArray( state.viewport.eye );
// {x: -14.870469093322754, y: 36.57156276702881, z: -1.212925910949707}

const originTarget = currentTarget.clone().applyMatrix4( invTransform );
// {x: -15.02436066552734, y: -8.984211875061035, z: 4.921260089050291}

const originPosition = currentPosition.clone().applyMatrix4( invTransform );
// {x: -15.12436009332275, y: -8.984616232971192, z: 4.921260089050291}

Then, we rename fields in JSON like the one below. (Please refer to DasRevitCameraSyncExtension#getViewState() for the details)

{
    "aspect": 1.7963206307490145,
    "isPerspective": true,
    "fov": 90.6808770984145,
    "position": [ -15.165047992449978,-8.997701015094862,4.916143307734757 ],
    "target": [ -15.02430824140946,-8.997131602441378,4.916143307734757 ],
    "up": [ 0,0,1 ],
    "orthoScale": 1
}

Now, we bring those data into Revit and calculate Revit 3D view orientation, with some extra steps. And, when restoring the APS viewer's camera, we don't create a new 3D view this time. Instead, we apply the changes to the current active Revit 3D view.

  1. We change the Revit view mode to orthographic by View3D#ToggleToIsometric() and to perspective by View3D#ToggleToPerspective() in Revit API. In addition, check if we can change the target view mode by View3D#CanToggleBetweenPerspectiveAndIsometric(), since the source camera mode from the viewer is not always consistent with the target Revit 3D view.
  2. As we discussed in the previous blog post, there is no much direct Revit API for us to change Revit camera parameters, but fortunately, we have UIView#ZoomAndCenterRectangle( XYZ viewCorner1, XYZ viewCorner2 ) to help us achieve this, and we can calculate zoom corners by those data. 
  3. Apply changes we made back to the view by UIDocument#RefreshActiveView() and UIDocument#UpdateAllOpenViews().
using (var trans = new Transaction(doc, "Restore Web Viewer Camera"))
{
	try
	{
		if (trans.Start() == TransactionStatus.Started)
		{
			var isPerspective = this.ViewState.IsPerspective;

			if (!view3D.CanToggleBetweenPerspectiveAndIsometric())
				throw new InvalidOperationException("Cannot change view projection");

			if (view3D.IsPerspective && !isPerspective)
				view3D.ToggleToIsometric();

			if (!view3D.IsPerspective && isPerspective)
				view3D.ToggleToPerspective();

			// By default, the 3D view uses a default orientation.
			// Change the orientation by creating and setting a ViewOrientation3D 
			var position = new XYZ(this.ViewState.Position[0], this.ViewState.Position[1], this.ViewState.Position[2]);
			var target = new XYZ(this.ViewState.Target[0], this.ViewState.Target[1], this.ViewState.Target[2]);
			var up = new XYZ(this.ViewState.Up[0], this.ViewState.Up[1], this.ViewState.Up[2]);
			var fov = this.ViewState.FieldOfView;
			var aspectRatio = this.ViewState.Aspect;
			var orthographicHeight = this.ViewState.OrthoScale;

			var sightDir = target.Subtract(position).Normalize();
			var right = sightDir.CrossProduct(up);
			var adjustedUp = right.CrossProduct(sightDir);

			var orientation = new ViewOrientation3D(position, adjustedUp, sightDir);
			view3D.SetOrientation(orientation);

			XYZ[] zoomCorners = null;
			if (!isPerspective)
			{
				zoomCorners = this.CalculateZoomCorners(view3D, position, target, orthographicHeight);
			}
			else
			{
				zoomCorners = this.CalculateZoomCorners(view3D, position, target, fov, aspectRatio);
			}


			var uiView = uiApp.ActiveUIDocument.GetOpenUIViews().FirstOrDefault(v => v.ViewId == view3D.Id);
			uiView.ZoomAndCenterRectangle(zoomCorners[0], zoomCorners[1]);

			uiApp.ActiveUIDocument.RefreshActiveView();
			uiApp.ActiveUIDocument.UpdateAllOpenViews();

			trans.Commit();
		}
	}
	catch (Exception ex)
	{
		trans.RollBack();
		TaskDialog.Show("Revit", "Failed to restore view state!");
	}
}

Here are the ways how I calculate the zoom corners. Special thanks to Valerii Nozdrenkov and my colleague Jeremy Tammik, who shared this wonderful tip in this blog post that helped me figure out the algorithm: The Building Coder - Save and Restore 3D View Camera Settings.

(Note. APS Viewer's FOV is the vertical field angle of the viewing frustum, according to https://threejs.org/docs/#api/en/cameras/PerspectiveCamera.fov, as APS Viewer is built on the top of the three.js)

// For Perspective Camera (Revit Perspective View)
private XYZ[] CalculateZoomCorners(View3D view, XYZ position, XYZ target, double fov, double aspectRatio)
{
	var sightVec = position - target;
	var halfHeight = Math.Tan((fov / 2) * Math.PI / 180) * sightVec.GetLength();
	var halfWidth = halfHeight * aspectRatio;

	var upDirectionVec = sightVec.CrossProduct(view.RightDirection).Normalize() * halfHeight;
	var rightDirectionVecOnPlane = sightVec.CrossProduct(upDirectionVec).Normalize() * halfWidth;
	var diagonalVec = upDirectionVec.Add(rightDirectionVecOnPlane);

	var corner1 = target.Add(diagonalVec);
	var corner2 = target.Add(diagonalVec.Negate());

	return new XYZ[]
	{
		corner1,
		corner2
	};
}

// For Orthographic Camera (Revit Isometric View)
private XYZ[] CalculateZoomCorners(View3D view, XYZ position, XYZ target, double orthographicHeight)
{
	var sightVec = position - target;
	var halfHeight = orthographicHeight / 2;
	var halfWidth = halfHeight;

	var upDirectionVec = sightVec.CrossProduct(view.RightDirection).Normalize() * halfHeight;
	var rightDirectionVecOnNearPlane = sightVec.CrossProduct(upDirectionVec).Normalize() * halfWidth;
	var diagonalVec = upDirectionVec.Add(rightDirectionVecOnNearPlane);

	var corner1 = target.Add(diagonalVec);
	var corner2 = target.Add(diagonalVec.Negate());

	return new XYZ[] {
	corner1,
	corner2
  };
}

ApsViewerRevitCameraSync-Formula-Orthographic

ApsViewerRevitCameraSync-Formula-Orthographic.png

ApsViewerRevitCameraSync-Formula-Zoom-Corners

Here is the demo video:

And source codes can be found here:

 

Enjoy it! If you're interested, here is link for part II: restore Revit camera on Viewer.

Related Article