Controlling camera in Forge viewer - autodesk-forge

I'm trying to control the camera in the Autodesk Forge Viewer. Setting target and position seems to work fine, but if I try to set rotation or quaternion it do not have any effect.
To get the camera I use the getCamera function and then applyCamera after I have tried to set the parameters.
What I'm trying to achieve is to use the device orientation on a handheld device to rotate the model. Just using alpha and beta to set target is not a smooth experience.
// get camera
var cam = _viewer.getCamera();
// get position
var vecPos = cam.position;
// get view vector
var vecViewDir = new THREE.Vector3();
vecViewDir.subVectors(cam.target,cam.position);
// get length of view vector
var length = vecViewDir.length();
// rotate alpha
var vec = new THREE.Vector3();
vec.y = length;
var zAxis = new THREE.Vector3(0,0,1);
vec.applyAxisAngle(zAxis,THREE.Math.degToRad(alpha));
// rotate beta
var vec2 = new THREE.Vector3(vec.x,vec.y,vec.z);
vec2.normalize();
vec2.negate();
vec2.cross(zAxis);
vec.applyAxisAngle(vec2,THREE.Math.degToRad(beta) + Math.PI / 2);
// add to camera
cam.target.addVectors(vecPos,vec);
_viewer.applyCamera(cam,false);

You need to use the setView() method
_viewer.navigation.setView (pos, target) ;
and may also need to set the up vector to make sure you orient the camera the way you want.
_viewer.navigation.setCameraUpVector (upVector) ;

Related

Autodesk Forge Viewer - Revit Location Point

Is there a way to get element position information from a Revit project viewed on Forge Viewer? We will be moving elements on Forge Viewer (mostly Family Instances) and I need to find out the new position of the elements after they are moved. It should match with the Revit API LocationPoint data.
When a design file (Revit, or any other supported file format) is processed by the Model Derivative service, and converted into the viewer format (SVF or SVF2), every element is turned into a "fragment" with its own transformation matrix. You can read (and even modify) the fragment information using the Viewer API:
const frags = viewer.model.getFragmentList();
function listFragmentProperties(fragId) {
console.log('Fragment ID:', fragId);
const objectIds = frags.getDbIds(fragId); // Get IDs of all objects linked to this fragment
console.log('Linked object IDs:', objectIds);
let matrix = new THREE.Matrix4();
frags.getWorldMatrix(fragId, matrix); // Get the fragment's world matrix
console.log('World matrix:', matrix);
let bbox = new THREE.Box3();
frags.getWorldBounds(fragId, bbox); // Get the fragment's world bounds
console.log('World bounds:', bbox);
}
And to modify the transformation of a fragment, try the following:
const frags = viewer.model.getFragmentList();
function modifyFragmentTransform(fragId) {
let scale = new THREE.Vector3();
let rotation = new THREE.Quaternion();
let translation = new THREE.Vector3();
frags.getAnimTransform(fragId, scale, rotation, translation);
translation.z += 10.0;
scale.x = scale.y = scale.z = 1.0;
frags.updateAnimTransform(fragId, scale, rotation, translation);
}

crop particular room/area from the forge viewer based on Revit coordinates(min and max X, Y, Z)

We are using forge viewer(v7) in our web application.
Our requirement is to crop particular room/area from the forge viewer. For example, if we have shown a house model in the forge viewer then if a user select a kitchen(from menu or navbar) then the viewer should show only kitchen area (including all its objects like cabinets, burner, fridge, sink etc.) and all other objects/sections should be hidden. Similarly for bedrooms, baths etc. It will be just for viewing purpose at run time and not for any automation.
We are getting room coordinates(min and max X, Y, Z) with the help of following using forge API(with Revit engine).
GeometryElement geoElement = room.ClosedShell;
BoundingBoxXYZ boundingBox = geoElement.GetBoundingBox();
XYZ min = boundingBox.Min;
XYZ max = boundingBox.Max;
We are using viewer.setCutPlanes function to draw cutplanes in viewer.
var minPt = new THREE.Vector3(x,y,z); //!<<< put your point here
var maxPt = new THREE.Vector3(x,y,z); //!<<< put your point here
const normals = [
new THREE.Vector3(1, 0, 0),
new THREE.Vector3(0, 1, 0),
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(0, -1, 0),
new THREE.Vector3(0, 0, -1)
];
const bbox = new THREE.Box3(minPt, maxPt);
const cutPlanes = [];
for (let i = 0; i < normals.length; i++) {
const plane = new THREE.Plane(normals[i], -1 * maxPt.dot(normals[i]));
// offset plane with negative normal to form an octant
if (i > 2) {
const ptMax = plane.orthoPoint(bbox.max);
const ptMin = plane.orthoPoint(bbox.min);
const size = new THREE.Vector3().subVectors(ptMax, ptMin);
plane.constant -= size.length();
}
const n = new THREE.Vector4(plane.normal.x, plane.normal.y, plane.normal.z, plane.constant);
cutPlanes.push(n);
}
viewer.setCutPlanes(cutPlanes);
But when we are passing these coordinates (obtained for API) to this front end JS code the cutPlanes are getting created at incorrect position/points. For example when we are passing coordinates of kitchen its cropping the small portion of roof and same with all other room.
The possible reason is that the Revit & forge viewer coordinates are not same.
Does anyone have an idea that how can we map these Revit coordinates with forge viewer and draw cutplanes?
If you're following the Forge Viewer tutorial to load the model, then you need to subtract global offsets from endpoints of the room bounding box like below:
var minPt = new THREE.Vector3(x,y,z); //!<<< put your point here
var maxPt = new THREE.Vector3(x,y,z); //!<<< put your point here
var offsetMatrix = viewer.model.getData().placementWithOffset;
var offsetMinPt = minPt.applyMatrix4(offsetMatrix);
var offsetMaxPt = maxPt.applyMatrix4(offsetMatrix);
i have another solution. modify model by hand like cut plane, hide, isolate element to retrieve view you want to show. Then use method var data = viewer.getState() and store that data to your database. then use viewer.restoreState(data) to recall your view.

Autodesk Forge: programmatically zoom out after focus on object by fitToView()

After select and focusing on an object with fitToView(objectIds, model),
I want to zoom out the camera programatically instead of via mouse events.
I tried the following code and it was able to zoom out, but the 3D space was distorted.
If you know how to correctly zoom out, please let me know.
// select and isolate target object
let dbid = [1137]
viewer.impl.visibilityManager.isolate(dbid, viewer.model)
viewer.select(dbid)
//focus on target object
viewer.getAggregateSelection((data) => {
let rootId = data.selector.getInstanceTree().nodeAccess.rootId
viewer.fitToView(rootId, viewer.model)
})
//zoom out camera
let cam = viewer.getCamera().perspectiveCamera
cam.zoom *= 0.05
viewer.impl.syncCamera();
It's not the zoom property that you need to change but the camera position:
var nav = viewer.navigation
var pos = nav.getPosition()
var target = nav.getTarget()
var viewdir = new THREE.Vector3()
viewdir.subVectors (pos, target).normalize()
// zooms out by 100 along the view direction
viewdir.multiplyScalar (100)
pos.add(viewdir)
nav.setPosition(pos)
It's not the zoom property that you need to change but set the camera position:
let camera = this.viewer3D.getCamera();
viewer.fitToView(dbIds, null, true); //provide your device id to locate
var direction = new THREE.Vector3();
camera.getWorldDirection( direction );
//set distance move the camera forward to your needs (gives value in minus for zoomout)
camera.position.add( direction.multiplyScalar(-15));
let fov = viewer.navigation.getVerticalFov(); //or horizontalFov
viewer.navigation.setRequestTransition(true, camera.position,viewer.navigation.getTarget(), fov);
//if not work then try fov + 20,30,40 etc..

How can I get 3D object dimensions in ThreeJS?

I'm developing an AR application using WebRTC (webcam access), JSARToolKit (marker detection) and threeJS (3D library).
I want to place 3D objects (exported from Maya using threejs maya exporter) in the center of the detected marker.
This is the code where I load the 3D object using JSONLoader:
// load the model
var loader = new THREE.JSONLoader;
var object;
//var geometry = new THREE.BoxGeometry(1, 1, 1);
loader.load('js/cube.js', function(geometry, materials){
var material = new THREE.MeshFaceMaterial(materials);
object = new THREE.Mesh(geometry, material);
object.position.x -= ***3DobjectWidth/2***;
object.position.y -= ***3DobjectHeight/2***;
object.position.z -= ***3DobjectDepth/2***;
scene.add(object);
});
I need to get width, height and depth of the object to change his position (see 3DobjectWidth ecc).
Any suggestions?
The object size will be placed at geometry.boundingBox. But it has to be generated once.
try this.
geometry.computeBoundingBox();
var bb = geometry.boundingBox;
var object3DWidth = bb.max.x - bb.min.x;
var object3DHeight = bb.max.y - bb.min.y;
var object3DDepth = bb.max.z - bb.min.z;
Geometry has a .center() method. That might be more efficient and simpler if you need to center many meshes based on same geometry. It also calls computeBoundingBox for you.

Dynamic google map with custom tiles prevent repeating pan

I have a dynamic tile set where I do NOT want to allow panning outside of its bounds.
The below code gets me close, but the user can still scroll horizontally outside of strict bounds because it uses the map center for comparison
var strictBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(sw_lat, sw_lon),
new google.maps.LatLng(ne_lat, ne_lon)
);
google.maps.event.addListener(map, 'drag', function()
{
if (strictBounds.contains(map.getCenter())) return;
// We're out of bounds - Move the map back within the bounds
var c = map.getCenter(),
x = c.lng(),
y = c.lat(),
maxX = strictBounds.getNorthEast().lng(),
maxY = strictBounds.getNorthEast().lat(),
minX = strictBounds.getSouthWest().lng(),
minY = strictBounds.getSouthWest().lat();
if (x < minX) x = minX;
if (x > maxX) x = maxX;
if (y < minY) y = minY;
if (y > maxY) y = maxY;
map.setCenter(new google.maps.LatLng(y, x));
});
For quick access, here is the jsfiddle for this solution: http://jsfiddle.net/nYz6k/
Supposed you're happy with the current solution of detecting bounds against the center, all you have to do is to restrict the bounds based on the current map canvas size.
The result that you're getting now is that every corner of the restricting bounds appears on the center of the map when it's full restricting (i.e. both latitude and longitude exceed the bounds corners).
The solution is to actually remove the x and y offset from the bounds, and still be able to check against the center (which is the most efficient solution, compared to other bounds-based checking solutions).
Also, you have to restrict the bounds only when you initialize the map and when the window is resizing, which means that when you pan there is no extra processing overhead besides the check method that you have already provided.
Don't forget to set the maxZoom property for the map (tweak it as needed) because above a determined zoom level the restricting bounds will by themselves fit in the viewport and there's no solution for that.
Important! Use 'center_changed' instead of 'drag' because 'drag' has that sliding behavior that when you finished dragging and set the center, the maps still slides in the direction of the panning.
Before you implement the solution I recommend you to check out how the Maps API coordinate system and projection works, because this solution is heavily based on it, and if you want to tweak it out, it's useful to know this information.
https://developers.google.com/maps/documentation/javascript/maptypes and check out the Custom Map Types -> Map Coordinates section.
Here's what you need to do. First, you need to implement 2 short methods of converting between geographic coordinates (LatLng) and pixel coordinates.
var fromLatLngToPixel = function (latLng) {
var point = map.getProjection().fromLatLngToPoint(latLng);
var zoom = map.getZoom();
return new google.maps.Point(
Math.floor(point.x * Math.pow(2, zoom)),
Math.floor(point.y * Math.pow(2, zoom))
);
}
var fromPixelToLatLng = function (pixel) {
var zoom = map.getZoom();
var point = new google.maps.Point(
pixel.x / Math.pow(2, zoom),
pixel.y / Math.pow(2, zoom)
);
return map.getProjection().fromPointToLatLng(point);
}
Next, implement the method that effectively restricts the bounds. Note that you always have to keep a variable that stores the original bounds, because each time the map canvas is resized, the resulting bounds will change.
For this, let's say originalBounds keeps the bounds you provided with sw_lat, sw_long etc, and shrinkedBounds is modified by this method. You can rename it to strictBounds to still work in your method, but it's up to you. This uses jQuery for getting the width and height of the canvas object.
var shrinkBounds = function () {
zoom = map.getZoom();
// The x and y offset will always be half the current map canvas
// width and height respectively
xoffset = $('#map_canvas').width() / 2;
yoffset = $('#map_canvas').height() / 2;
// Convert the bounds extremities to global pixel coordinates
var pixswOriginal = fromLatLngToPixel(originalBounds.getSouthWest());
var pixneOriginal = fromLatLngToPixel(originalBounds.getNorthEast());
// Shrink the original bounds with the x and y offset
var pixswShrinked = new google.maps.Point(pixswOriginal.x + xoffset, pixswOriginal.y - yoffset);
var pixneShrinked = new google.maps.Point(pixneOriginal.x - xoffset, pixneOriginal.y + yoffset);
// Rebuild the shrinkedBounds object with the modified
shrinkedBounds = new google.maps.LatLngBounds(
fromPixelToLatLng(pixswShrinked),
fromPixelToLatLng(pixneShrinked));
}
Next, all you have to do is to call this method:
once when the map is initialized. Be aware that depending on when you call the method, you may be getting some weird errors because the map might not have yet initialized all it's properties. Best way is to use the projection_changed event.
google.maps.event.addListener(map, 'projection_changed', function (e) {
shrinkBounds();
});
every time the map canvas is resized
google.maps.event.addListener(map, 'resize', function (e) {
shrinkBounds();
});
But the most important part of this is that the 'resize' event is never triggered for programmatic resizes of the map container, so you have to trigger it manually every time you resize the canvas programmatically (if you do, but I doubt you do).
The most common way is to trigger it when the window is resized:
$(window).resize(function () {
google.maps.event.trigger(map, 'resize');
});
After all of this, you can now safely use your method, but as a handler for 'center_changed' and not for 'drag' as I've mentioned earlier.
I set up a jsfiddle with the full working code.
http://jsfiddle.net/nYz6k/
You can see the restricting at the bottom left corner with that little half-marker that shows up, which is positioned in the SW corner of the bounds. There is also one at the NW corner but naturally you can't see it because it's displayed above the position.
Instead of using map.getCenter() as a check, use map.getBounds()
Untested, but a simple way to accomplish this would be to check if the union of both bounds were identical (if not, your map bounds is outside your strict bounds):
var union = strictBounds.union(map.getBounds());
if (strictBounds.equals(union)) { //new map bounds is within strict bounds
Tiborg version above is the best version so far in keeping the bounds...
I noticed a bug that occurs when zoom level and width/height are larger than original bounds
I have modified his code and added a rectangle to see it in action.
var buildBounds = function(sw, ne) {
var swY = sw.lat();
var swX = sw.lng();
var neY = ne.lat();
var neX = ne.lng();
if (swY > neY) {
var cY = (swY + neY) / 2;
swY = cY;
neY = cY;
}
if (swX > neX) {
var cX = (swX + neX) / 2;
swX = cX;
neX = cX;
}
return new google.maps.LatLngBounds(
new google.maps.LatLng(swY, swX),
new google.maps.LatLng(neY, neX));
}
http://jsfiddle.net/p4wgjs6s/