/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.controllers; import com.jaamsim.basicsim.Simulation; import com.jaamsim.math.Mat4d; import com.jaamsim.math.MathUtils; import com.jaamsim.math.Plane; import com.jaamsim.math.Quaternion; import com.jaamsim.math.Ray; import com.jaamsim.math.Transform; import com.jaamsim.math.Vec3d; import com.jaamsim.math.Vec4d; import com.jaamsim.render.CameraInfo; import com.jaamsim.render.RenderUtils; import com.jaamsim.render.Renderer; import com.jaamsim.render.WindowInteractionListener; import com.jaamsim.ui.GUIFrame; import com.jaamsim.ui.View; import com.jogamp.newt.event.KeyEvent; import com.jogamp.newt.event.MouseEvent; public class CameraControl implements WindowInteractionListener { private static final double ZOOM_FACTOR = 1.1; // Scale from pixels dragged to radians rotated private static final double ROT_SCALE_X = 0.005; private static final double ROT_SCALE_Z = 0.005; private final Renderer _renderer; private int _windowID; private final View _updateView; private final Vec3d POI = new Vec3d(); private static class PolarInfo { double rotZ; // The spherical coordinate that rotates around Z (in radians) double rotX; // Ditto for X double radius; // The distance the camera is from the view center final Vec3d viewCenter; PolarInfo(Vec3d center) { viewCenter = new Vec3d(center); } @Override public boolean equals(Object o) { if (!(o instanceof PolarInfo)) { return false; } PolarInfo pi = (PolarInfo)o; return pi.rotZ == rotZ && pi.rotX == rotX && pi.radius == radius && viewCenter.equals3(pi.viewCenter); } } private PolarInfo piCache; // The last polar info this view has re-drawn for public CameraControl(Renderer renderer, View updateView) { _renderer = renderer; _updateView = updateView; POI.set3(_updateView.getGlobalCenter()); } @Override public void mouseDragged(WindowInteractionListener.DragInfo dragInfo) { // Give the RenderManager first crack at this if (RenderManager.inst().handleDrag(dragInfo)) { RenderManager.redraw(); return; // Handled } if (!_updateView.isMovable() || _updateView.isScripted()) { return; } if (dragInfo.button == 1) { if (dragInfo.shiftDown()) { handleExpVertPan(dragInfo.x, dragInfo.y, dragInfo.dx, dragInfo.dy); } else { handleExpPan(dragInfo.x, dragInfo.y, dragInfo.dx, dragInfo.dy); } } else if (dragInfo.button == 3 && !_updateView.is2DLocked()) { if (dragInfo.shiftDown()) { handleTurnCamera(dragInfo.dx, dragInfo.dy); } else { handleRotAroundPoint(dragInfo.x, dragInfo.y, dragInfo.dx, dragInfo.dy); } } } private void handleTurnCamera(int dx, int dy) { Vec3d camPos = _updateView.getGlobalPosition(); Vec3d center = _updateView.getGlobalCenter(); PolarInfo origPi = getPolarFrom(center, camPos); Quaternion origRot = polarToRot(origPi); Mat4d rot = new Mat4d(); rot.setRot3(origRot); Vec3d rotXAxis = new Vec3d(1.0d, 0.0d, 0.0d); rotXAxis.mult3(rot, rotXAxis); Quaternion rotX = new Quaternion(); rotX.setAxisAngle(rotXAxis, dy * ROT_SCALE_X / 4); Quaternion rotZ = new Quaternion(); rotZ.setRotZAxis(dx * ROT_SCALE_Z / 4); Mat4d rotTransX = MathUtils.rotateAroundPoint(rotX, camPos); Mat4d rotTransZ = MathUtils.rotateAroundPoint(rotZ, camPos); center.multAndTrans3(rotTransX, center); center.multAndTrans3(rotTransZ, center); PolarInfo pi = getPolarFrom(center, camPos); updateCamTrans(pi, true); } private void handleExpPan(int x, int y, int dx, int dy) { Renderer.WindowMouseInfo info = _renderer.getMouseInfo(_windowID); if (info == null) return; //Cast a ray into the XY plane both for now, and for the previous mouse position Ray currRay = RenderUtils.getPickRayForPosition(info.cameraInfo, x, y, info.width, info.height); Ray prevRay = RenderUtils.getPickRayForPosition(info.cameraInfo, x - dx, y - dy, info.width, info.height); double currZDot = currRay.getDirRef().z; double prevZDot = prevRay.getDirRef().z; if (Math.abs(currZDot) < 0.017 || Math.abs(prevZDot) < 0.017) // 0.017 is roughly sin(1 degree) { // This is too close to the xy-plane and will lead to too wild a translation return; } Plane dragPlane = new Plane(null, POI.z); double currDist = dragPlane.collisionDist(currRay); double prevDist = dragPlane.collisionDist(prevRay); if (currDist < 0 || prevDist < 0 || currDist == Double.POSITIVE_INFINITY || prevDist == Double.POSITIVE_INFINITY) { // We're either parallel to or beneath the collision plane, bail out return; } Vec3d currIntersect = currRay.getPointAtDist(currDist); Vec3d prevIntersect = prevRay.getPointAtDist(prevDist); Vec3d diff = new Vec3d(); diff.sub3(currIntersect, prevIntersect); Vec3d camPos = _updateView.getGlobalPosition(); Vec3d center = _updateView.getGlobalCenter(); camPos.sub3(diff); center.sub3(diff); PolarInfo pi = getPolarFrom(center, camPos); updateCamTrans(pi, true); } private void handleExpVertPan(int x, int y, int dx, int dy) { Renderer.WindowMouseInfo info = _renderer.getMouseInfo(_windowID); if (info == null) return; //Cast a ray into the XY plane both for now, and for the previous mouse position Ray currRay = RenderUtils.getPickRayForPosition(info.cameraInfo, x, y, info.width, info.height); Ray prevRay = RenderUtils.getPickRayForPosition(info.cameraInfo, x - dx, y - dy, info.width, info.height); double zDiff = RenderUtils.getZDiff(POI, currRay, prevRay); Vec3d camPos = _updateView.getGlobalPosition(); Vec3d center = _updateView.getGlobalCenter(); camPos.z -= zDiff; center.z -= zDiff; PolarInfo pi = getPolarFrom(center, camPos); updateCamTrans(pi, true); } private void handleRotAroundPoint(int x, int y, int dx, int dy) { Vec3d camPos = _updateView.getGlobalPosition(); Vec3d center = _updateView.getGlobalCenter(); PolarInfo origPi = getPolarFrom(center, camPos); if ( camPos.x == center.x && camPos.y == center.y ) { // This is a degenerate camera view, tweak the polar info a bit to // prevent view flipping origPi.rotX = 0.00001; origPi.rotZ = 0; } Quaternion origRot = polarToRot(origPi); Mat4d rot = new Mat4d(); rot.setRot3(origRot); Vec3d origUp = new Vec3d(0.0d, 1.0d, 0.0d); origUp.mult3(rot, origUp); Vec3d rotXAxis = new Vec3d(1.0d, 0.0d, 0.0d); rotXAxis.mult3(rot, rotXAxis); Quaternion rotX = new Quaternion(); rotX.setAxisAngle(rotXAxis, -dy * ROT_SCALE_X); Quaternion rotZ = new Quaternion(); rotZ.setRotZAxis(-dx * ROT_SCALE_Z); Mat4d rotTransX = MathUtils.rotateAroundPoint(rotX, POI); Mat4d rotTransZ = MathUtils.rotateAroundPoint(rotZ, POI); camPos.multAndTrans3(rotTransX, camPos); center.multAndTrans3(rotTransX, center); camPos.multAndTrans3(rotTransZ, camPos); center.multAndTrans3(rotTransZ, center); PolarInfo pi = getPolarFrom(center, camPos); Quaternion newRot = polarToRot(pi); rot.setRot3(newRot); Vec3d newUp = new Vec3d(0.0d, 1.0d, 0.0d); newUp.mult3(rot, newUp); double upDot = origUp.dot3(newUp); if (upDot < 0) { // The up angle has changed by more than 90 degrees, we probably are looking directly up or down // Instead only apply the rotation around Z camPos = _updateView.getGlobalPosition(); center = _updateView.getGlobalCenter(); camPos.multAndTrans3(rotTransZ, camPos); center.multAndTrans3(rotTransZ, center); pi = getPolarFrom(center, camPos); } updateCamTrans(pi, true); } @Override public void mouseWheelMoved(int windowID, int x, int y, int wheelRotation, int modifiers) { if (!_updateView.isMovable() || _updateView.isScripted()) { return; } Vec3d camPos = _updateView.getGlobalPosition(); Vec3d center = _updateView.getGlobalCenter(); Vec3d diff = new Vec3d(); diff.sub3(POI, camPos); double scale = 1; double zoomFactor = (wheelRotation > 0) ? 1/ZOOM_FACTOR : ZOOM_FACTOR; for (int i = 0; i < Math.abs(wheelRotation); ++i) { scale = scale * zoomFactor; } // offset is the difference from where we are to where we're going diff.scale3(1 - scale); camPos.add3(diff); center.add3(diff); PolarInfo pi = getPolarFrom(center, camPos); updateCamTrans(pi, true); } @Override public void mouseClicked(int windowID, int x, int y, int button, int modifiers, short count) { if (!RenderManager.isGood()) { return; } RenderManager.inst().hideExistingPopups(); if (button == 3) { // Hand this off to the RenderManager to deal with RenderManager.inst().popupMenu(windowID); } if (button == 1 && (modifiers & WindowInteractionListener.MOD_CTRL) == 0) { RenderManager.inst().handleMouseClicked(windowID, x, y, count); } } @Override public void mouseMoved(int windowID, int x, int y) { if (!RenderManager.isGood()) { return; } RenderManager.redraw(); RenderManager.inst().mouseMoved(windowID, x, y); } @Override public void rawMouseEvent(MouseEvent me) { } @Override public void mouseEntry(int windowID, int x, int y, boolean isInWindow) { if (!RenderManager.isGood()) { return; } if (isInWindow && RenderManager.inst().isDragAndDropping()) { RenderManager.inst().createDNDObject(windowID, x, y); } } private Quaternion polarToRot(PolarInfo pi) { Quaternion rot = new Quaternion(); rot.setRotZAxis(pi.rotZ); Quaternion tmp = new Quaternion(); tmp.setRotXAxis(pi.rotX); rot.mult(rot, tmp); return rot; } private void updateCamTrans(PolarInfo pi, boolean updateInputs) { if (pi.rotX == 0) { pi.rotZ = 0; // If we're ever looking directly down, which is degenerate, force Y up } if (_updateView.is2DLocked()) { pi.rotX = 0; pi.rotZ = 0; } if (piCache != null && piCache.equals(pi) && !updateInputs) { return; // This update won't do anything } piCache = pi; Vec4d zOffset = new Vec4d(0, 0, pi.radius, 1.0d); Quaternion rot = polarToRot(pi); Transform finalTrans = new Transform(pi.viewCenter); finalTrans.merge(finalTrans, new Transform(null, rot, 1)); finalTrans.merge(finalTrans, new Transform(zOffset)); if (updateInputs) { updateViewPos(finalTrans.getTransRef(), pi.viewCenter); } // Finally update the renders camera info CameraInfo info = _renderer.getCameraInfo(_windowID); if (info == null) { // This window has not been opened yet (or is closed) force a redraw as everything will catch up // and the information has been saved to the view object RenderManager.redraw(); piCache = null; return; } info.trans = finalTrans; info.skyboxTexture = _updateView.getSkyboxTexture(); _renderer.setCameraInfoForWindow(_windowID, info); // Queue a redraw RenderManager.redraw(); } public void setRotationAngles(double rotX, double rotZ) { PolarInfo pi = getPolarCoordsFromView(); pi.rotX = rotX; pi.rotZ = rotZ; updateCamTrans(pi, true); } public void setWindowID(int windowID) { _windowID = windowID; } @Override public void windowClosing() { if (!RenderManager.isGood()) { return; } RenderManager.inst().hideExistingPopups(); RenderManager.inst().windowClosed(_windowID); } @Override public void mouseButtonDown(int windowID, int x, int y, int button, boolean isDown, int modifiers) { if (!RenderManager.isGood()) { return; } // We need to cache dragging if (button == 1 && isDown) { Vec3d clickPoint = RenderManager.inst().getNearestPick(_windowID); if (clickPoint != null) { POI.set3(clickPoint); //dragPlane = new Plane(Vec4d.Z_AXIS, clickPoint.z); } else { // Set the drag plane to the XY_PLANE Renderer.WindowMouseInfo info = _renderer.getMouseInfo(_windowID); if (info == null) return; //Cast a ray into the XY plane both for now, and for the previous mouse position Ray mouseRay = RenderUtils.getPickRayForPosition(info.cameraInfo, x, y, info.width, info.height); double dist = RenderManager.XY_PLANE.collisionDist(mouseRay); if (dist < 0) { return; } POI.set3(mouseRay.getPointAtDist(dist)); //dragPlane = Plane.XY_PLANE; } } RenderManager.inst().handleMouseButton(windowID, x, y, button, isDown, modifiers); } @Override public void windowGainedFocus() { if (!RenderManager.isGood()) { return; } RenderManager.inst().setActiveWindow(_windowID); } /** * Set the position information in the saved view to match this window */ private void updateViewPos(Vec3d viewPos, Vec3d viewCenter) { if (_updateView == null) { return; } _updateView.updateCenterAndPos(viewCenter, viewPos); GUIFrame.updateUI(); } @Override public void windowMoved(int x, int y, int width, int height) { // Filter out large negative values occuring from window minimize: if (x < -30000 || y < - 30000) return; _updateView.setWindowPos(x, y, width, height); } public View getView() { return _updateView; } private PolarInfo getPolarFrom(Vec3d center, Vec3d pos) { PolarInfo pi = new PolarInfo(center); Vec3d viewDiff = new Vec3d(); viewDiff.sub3(pos, pi.viewCenter); pi.radius = viewDiff.mag3(); pi.rotZ = Math.atan2(viewDiff.x, -viewDiff.y); double xyDist = Math.hypot(viewDiff.x, viewDiff.y); pi.rotX = Math.atan2(xyDist, viewDiff.z); return pi; } private PolarInfo getPolarCoordsFromView() { return getPolarFrom(_updateView.getGlobalCenter(), _updateView.getGlobalPosition()); } public void checkForUpdate() { PolarInfo pi = getPolarCoordsFromView(); updateCamTrans(pi, false); } @Override public void keyPressed(KeyEvent e) { // If an entity has been selected, pass the key event to it if (RenderManager.inst().isEntitySelected()) { RenderManager.inst().handleKeyPressed(e.getKeyCode(), e.getKeyChar(), e.isShiftDown(), e.isControlDown(), e.isAltDown()); return; } // If no entity has been selected, the camera will handle the key event Vec3d pos = _updateView.getGlobalPosition(); Vec3d cent = _updateView.getGlobalCenter(); // Construct a unit vector in the x-y plane in the direction of the view center Vec3d forward = new Vec3d(cent); forward.sub3(pos); forward.z = 0.0d; forward.normalize3(); // Trap the degenerate case where the camera look straight down on the x-y plane // For this case the normalize3 method returns a unit vector in the z-direction if (forward.z > 0.0) forward.set3(1.0d, 0.0d, 0.0d); // Construct a unit vector pointing to the left of the direction vector Vec3d left = new Vec3d( -forward.y, forward.x, 0.0d); // Scale the two vectors to the desired step size double inc = Simulation.getIncrementSize(); forward.scale3(inc); left.scale3(inc); int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) { pos.add3(left); cent.add3(left); } else if (keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) { pos.sub3(left); cent.sub3(left); } else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) { if (e.isShiftDown()) { pos.set3(pos.x, pos.y, pos.z+inc); cent.set3(cent.x, cent.y, cent.z+inc); } else { pos.add3(forward); cent.add3(forward); } } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) { if (e.isShiftDown()) { pos.set3(pos.x, pos.y, pos.z-inc); cent.set3(cent.x, cent.y, cent.z-inc); } else { pos.sub3(forward); cent.sub3(forward); } } else return; _updateView.updateCenterAndPos(cent, pos); } @Override public void keyReleased(KeyEvent e) { if (RenderManager.inst().isEntitySelected()) { RenderManager.inst().handleKeyReleased(e.getKeyCode(), e.getKeyChar(), e.isShiftDown(), e.isControlDown(), e.isAltDown()); return; } } }