heronarts.glx.ui.UI3dContext Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of glx Show documentation
Show all versions of glx Show documentation
UI harness for the LX framework based upon LWJGL + bgfx.
The newest version!
/**
* Copyright 2013- Mark C. Slee, Heron Arts LLC
*
* This file is part of the LX Studio software library. By using
* LX, you agree to the terms of the LX Studio Software License
* and Distribution Agreement, available at: http://lx.studio/license
*
* Please note that the LX license is not open-source. The license
* allows for free, non-commercial use.
*
* HERON ARTS MAKES NO WARRANTY, EXPRESS, IMPLIED, STATUTORY, OR
* OTHERWISE, AND SPECIFICALLY DISCLAIMS ANY WARRANTY OF
* MERCHANTABILITY, NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR
* PURPOSE, WITH RESPECT TO THE SOFTWARE.
*
* @author Mark C. Slee
*/
package heronarts.glx.ui;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.bgfx.BGFX;
import com.google.gson.JsonObject;
import heronarts.glx.View;
import heronarts.glx.event.KeyEvent;
import heronarts.glx.event.MouseEvent;
import heronarts.glx.ui.component.UIInputBox;
import heronarts.lx.LX;
import heronarts.lx.LXSerializable;
import heronarts.lx.modulator.Click;
import heronarts.lx.modulator.DampedParameter;
import heronarts.lx.modulator.LXPeriodicModulator;
import heronarts.lx.parameter.BooleanParameter;
import heronarts.lx.parameter.BoundedParameter;
import heronarts.lx.parameter.EnumParameter;
import heronarts.lx.parameter.LXParameter;
import heronarts.lx.parameter.MutableParameter;
import heronarts.lx.parameter.ObjectParameter;
import heronarts.lx.utils.LXUtils;
/**
* This is a layer that contains a 3d scene with a camera. Mouse movements
* control the camera, and the scene can contain components.
*/
public class UI3dContext extends UIObject implements LXSerializable, UILayer, UITabFocus {
public static final int NUM_CAMERA_POSITIONS = 6;
public static interface MovementListener {
public void reset();
public void translate(float x, float y, float z);
public void rotate(float theta, float phi);
}
/**
* Mode of mouse interaction
*/
public enum MouseMode {
/**
* Mouse dragging events alter the camera view
*/
VIEW,
/**
* Mouse dragging events invoke object movement callbacks
*/
OBJECT;
@Override
public String toString() {
switch (this) {
case OBJECT:
return "Move Fixtures";
default:
case VIEW:
return "Move Camera";
}
}
}
public enum ProjectionMode {
/**
* Perspective projection
*/
PERSPECTIVE,
/**
* Orthographic projection
*/
ORTHOGRAPHIC;
@Override
public String toString() {
switch (this) {
case ORTHOGRAPHIC:
return "Orthographic";
case PERSPECTIVE:
default:
return "Perspective";
}
}
};
/**
* Mouse interaction mode
*/
public EnumParameter mouseMode =
new EnumParameter("Mouse Mode", MouseMode.VIEW)
.setDescription("Mouse interaction mode");
/**
* Projection mode
*/
public final EnumParameter projection =
new EnumParameter("Projection", ProjectionMode.PERSPECTIVE)
.setDescription("Projection mode");
/**
* Perspective of view
*/
public final BoundedParameter perspective =
new BoundedParameter("Perspective", 60, 15, 150)
.setExponent(2)
.setUnits(BoundedParameter.Units.DEGREES)
.setDescription("Camera lens perspective in degrees");
/**
* Depth of perspective field, exponential factor of radius by exp(10, Depth)
*/
public final BoundedParameter depth =
new BoundedParameter("Depth", 2, 0, 10)
.setDescription("Camera's depth of perspective field");
/**
* Whether to animate between camera positions
*/
public final BooleanParameter animation =
new BooleanParameter("Animation", false)
.setDescription("Whether animation between camera positions is enabled");
/**
* Animation time
*/
public final BoundedParameter animationTime =
new BoundedParameter("Animation Time", 1000, 100, 300000)
.setExponent(2)
.setUnits(LXParameter.Units.MILLISECONDS)
.setDescription("Animation duration between camera positions");
/**
* Max velocity used to damp changes to radius (zoom)
*/
public final MutableParameter cameraVelocity = new MutableParameter("CVel", Float.MAX_VALUE);
/**
* Acceleration used to change camera radius (zoom)
*/
public final MutableParameter cameraAcceleration = new MutableParameter("CAcl", 0);
/**
* Max velocity used to damp changes to rotation (theta/phi)
*/
public final MutableParameter rotationVelocity = new MutableParameter("RVel", 16*180);
/**
* Acceleration used to change rotation (theta/phi)
*/
public final MutableParameter rotationAcceleration = new MutableParameter("RAcl", 0);
/**
* List of movement listeners when in OBJECT mouse mode
*/
private final List movementListeners = new ArrayList();
public final void addMovementListener(MovementListener listener) {
Objects.requireNonNull(listener, "Cannot add null UI3dContext.MovementListener");
if (this.movementListeners.contains(listener)) {
throw new IllegalStateException("Cannot add duplicate UI3dContext.MovementListener: " + listener);
}
this.movementListeners.add(listener);
}
public final void removeMovementListener(MovementListener listener) {
if (!this.movementListeners.contains(listener)) {
throw new IllegalStateException("Cannot remove non-registered UI3dContext.MovementListener: " + listener);
}
this.movementListeners.remove(listener);
}
public class Camera implements LXSerializable {
private static final double RANGE_LIMIT = 100000000;
public final BooleanParameter active =
new BooleanParameter("Active", false)
.setDescription("Whether this camera view is active");
public final BoundedParameter theta =
new BoundedParameter("Theta", 0, 360)
.setWrappable(true)
.setUnits(BoundedParameter.Units.DEGREES)
.setDescription("Camera azimuth about the Y-axis");
public final BoundedParameter phi =
new BoundedParameter("Phi", 0, -89, 89)
.setUnits(BoundedParameter.Units.DEGREES)
.setDescription("Camera elevation from the XZ plane");
public final BoundedParameter radius =
new BoundedParameter("Radius", 120, 0, RANGE_LIMIT)
.setDescription("Camera radius");
public final BoundedParameter x =
new BoundedParameter("X", 0, -RANGE_LIMIT, RANGE_LIMIT)
.setDescription("Camera X position");
public final BoundedParameter y =
new BoundedParameter("Y", 0, -RANGE_LIMIT, RANGE_LIMIT)
.setDescription("Camera Y position");
public final BoundedParameter z =
new BoundedParameter("Z", 0, -RANGE_LIMIT, RANGE_LIMIT)
.setDescription("Camera Z position");
private Camera() {}
private void reset() {
this.theta.reset();
this.phi.reset();
this.radius.reset();
this.x.reset();
this.y.reset();
this.z.reset();
}
private void set(Camera that) {
set(that, true);
}
private void set(Camera that, boolean active) {
this.theta.setValue(that.theta.getValue());
this.phi.setValue(that.phi.getValue());
this.radius.setValue(that.radius.getValue());
this.x.setValue(that.x.getValue());
this.y.setValue(that.y.getValue());
this.z.setValue(that.z.getValue());
if (active) {
this.active.setValue(true);
}
}
private void lerp(Camera one, Camera two, double amt) {
double thetaOne = one.theta.getValue();
double thetaTwo = two.theta.getValue();
if (Math.abs(thetaOne - thetaTwo) > 180) {
if (thetaOne < thetaTwo) {
thetaOne += 360;
} else {
thetaTwo += 360;
}
}
this.theta.setValue(LXUtils.lerp(thetaOne, thetaTwo, amt) % 360.);
this.phi.setValue(LXUtils.lerp(one.phi.getValue(), two.phi.getValue(), amt));
this.radius.setValue(LXUtils.lerp(one.radius.getValue(), two.radius.getValue(), amt));
this.x.setValue(LXUtils.lerp(one.x.getValue(), two.x.getValue(), amt));
this.y.setValue(LXUtils.lerp(one.y.getValue(), two.y.getValue(), amt));
this.z.setValue(LXUtils.lerp(one.z.getValue(), two.z.getValue(), amt));
}
private static final String KEY_ACTIVE = "active";
private static final String KEY_RADIUS = "radius";
private static final String KEY_THETA = "theta";
private static final String KEY_PHI = "phi";
private static final String KEY_X = "x";
private static final String KEY_Y = "y";
private static final String KEY_Z = "z";
@Override
public void save(LX lx, JsonObject object) {
object.addProperty(KEY_ACTIVE, this.active.isOn());
object.addProperty(KEY_RADIUS, this.radius.getValue());
object.addProperty(KEY_THETA, this.theta.getValue());
object.addProperty(KEY_PHI, this.phi.getValue());
object.addProperty(KEY_X, this.x.getValue());
object.addProperty(KEY_Y, this.y.getValue());
object.addProperty(KEY_Z, this.z.getValue());
}
@Override
public void load(LX lx, JsonObject object) {
LXSerializable.Utils.loadBoolean(this.active, object, KEY_ACTIVE);
LXSerializable.Utils.loadDouble(this.radius, object, KEY_RADIUS);
LXSerializable.Utils.loadDouble(this.theta, object, KEY_THETA);
LXSerializable.Utils.loadDouble(this.phi, object, KEY_PHI);
LXSerializable.Utils.loadDouble(this.x, object, KEY_X);
LXSerializable.Utils.loadDouble(this.y, object, KEY_Y);
LXSerializable.Utils.loadDouble(this.z, object, KEY_Z);
}
}
public final Camera[] cue = new Camera[NUM_CAMERA_POSITIONS];
public final ObjectParameter focusCamera;
public final Camera camera = new Camera();
private Camera prevCamera = null;
private Camera cameraFrom = new Camera();
private Camera cameraTo = new Camera();
private final LXPeriodicModulator animating = new Click(this.animationTime).setLooping(false);
public final UIInputBox.ProgressIndicator animationProgress = new UIInputBox.ProgressIndicator() {
@Override
public boolean hasProgress() {
return animating.isRunning();
}
@Override
public double getProgress() {
return animating.getBasis();
}
};
private final DampedParameter thetaDamped =
new DampedParameter(this.camera.theta, this.rotationVelocity, this.rotationAcceleration)
.setModulus(360);
private final DampedParameter phiDamped =
new DampedParameter(this.camera.phi, this.rotationVelocity, this.rotationAcceleration);
private final DampedParameter radiusDamped =
new DampedParameter(this.camera.radius, this.cameraVelocity, this.cameraAcceleration);
private final DampedParameter xDamped = new DampedParameter(
this.camera.x, this.cameraVelocity, this.cameraAcceleration
);
private final DampedParameter yDamped = new DampedParameter(
this.camera.y, this.cameraVelocity, this.cameraAcceleration
);
private final DampedParameter zDamped = new DampedParameter(
this.camera.z, this.cameraVelocity, this.cameraAcceleration
);
// These are derived from positionX based upon the camera mode
private final Vector3f center = new Vector3f(0, 0, 0);
private final Vector3f eye = new Vector3f(0, 0, 0);
private final Vector3f centerDamped = new Vector3f(0, 0, 0);
private final Vector3f eyeDamped = new Vector3f(0, 0, 0);
private final Vector3f up = new Vector3f(0, 1, 0);
// Radius bounds
private float minRadius = 1, maxRadius = Float.MAX_VALUE;
protected final View view;
private float x;
private float y;
private float width;
private float height;
protected UI3dContext(UI ui, float x, float y, float w, float h) {
setUI(ui);
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.view = new View(ui.lx);
setViewRect();
for (int i = 0; i < this.cue.length; ++i) {
this.cue[i] = new Camera();
}
this.focusCamera = new ObjectParameter("Camera", this.cue);
addListener(this.focusCamera, p -> {
Camera selectCamera = this.focusCamera.getObject();
if (!selectCamera.active.isOn()) {
// Store state into the camera
selectCamera.set(this.camera);
} else {
if (this.animation.isOn() && (selectCamera != this.prevCamera)) {
// Trigger animation from current camera to the next
this.cameraFrom.set(this.camera);
this.cameraTo.set(selectCamera);
this.animating.trigger();
} else {
// Immediately update all camera state
this.animating.stop();
this.camera.set(selectCamera);
this.thetaDamped.setValue(this.camera.theta.getValue());
this.phiDamped.setValue(this.camera.phi.getValue());
this.radiusDamped.setValue(this.camera.radius.getValue());
this.xDamped.setValue(this.camera.x.getValue());
this.yDamped.setValue(this.camera.y.getValue());
this.zDamped.setValue(this.camera.z.getValue());
computeCamera(true);
}
}
this.prevCamera = selectCamera;
});
addLoopTask(this.animating);
addLoopTask(this.thetaDamped);
addLoopTask(this.phiDamped);
addLoopTask(this.radiusDamped);
addLoopTask(this.xDamped);
addLoopTask(this.yDamped);
addLoopTask(this.zDamped);
this.thetaDamped.start();
this.radiusDamped.start();
this.phiDamped.start();
this.xDamped.start();
this.yDamped.start();
this.zDamped.start();
computeCamera(true);
addListener(this.camera.radius, p -> {
double value = this.camera.radius.getValue();
if (value < this.minRadius || value > this.maxRadius) {
this.camera.radius.setValue(LXUtils.constrain(value, this.minRadius, this.maxRadius));
}
});
}
@Override
public void dispose() {
this.view.dispose();
super.dispose();
}
@Override
public float getX() {
return this.x;
}
@Override
public float getY() {
return this.y;
}
@Override
public float getWidth() {
return this.width;
}
@Override
public float getHeight() {
return this.height;
}
public UI3dContext setPosition(float x, float y) {
return setRect(x, y, this.width, this.height);
}
public UI3dContext setSize(float width, float height) {
return setRect(this.x, this.y, width, height);
}
public UI3dContext setRect(float x, float y, float width, float height) {
if (this.x != x || this.y != y || this.width != width || this.height != height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
setViewRect();
}
return this;
}
private void setViewRect() {
if (this.ui.lx.isOpenGL()) {
// NOTE(mcslee): I really have no clue what is up with the Y-values here, this was
// arrived at by horrendous trial and error
this.view.setRect(
(int) (this.x * this.ui.lx.getUIZoom()),
(int) ((this.ui.getHeight() + this.y) * this.ui.lx.getUIZoom()),
(int) (this.width * this.ui.lx.getUIZoom()),
(int) (this.height * this.ui.lx.getUIZoom())
);
} else {
// Note that we transform our rect by UI content scaling factor
this.view.setRect(
(int) (this.x * this.ui.getContentScaleX()),
(int) (this.y * this.ui.getContentScaleY()),
(int) Math.ceil(this.width * this.ui.getContentScaleX()),
(int) Math.ceil(this.height * this.ui.getContentScaleY())
);
}
}
/**
* Adds a component to the layer
*
* @param component Component
* @return this
*/
public final UI3dContext addComponent(UI3dComponent component) {
if (component.getContext() != null) {
throw new IllegalStateException("Cannot add 3d component to multiple contexts");
}
if (this.mutableChildren.contains(component)) {
throw new IllegalStateException("Cannot add 3d component twice");
}
component.setContext(this);
this.mutableChildren.add(component);
return this;
}
/**
* Removes a component from the layer
*
* @param component Component
* @return this
*/
public final UI3dContext removeComponent(UI3dComponent component) {
if (!this.mutableChildren.contains(component)) {
throw new IllegalStateException("Cannot remove 3d component that doens't belong to context");
}
this.mutableChildren.remove(component);
component.setContext(null);
return this;
}
/**
* Clears the camera stored at the given index
*
* @param index Camera index to clear
* @return this
*/
public UI3dContext clearCamera(int index) {
this.cue[index].active.setValue(false);
return this;
}
/**
* Sets the cue position index of the camera
*
* @param index Camera index
* @return this
*/
public UI3dContext setCamera(int index) {
if (this.focusCamera.getValuei() != index) {
this.focusCamera.setValue(index);
} else {
this.focusCamera.bang();
}
return this;
}
/**
* Set radius of the camera
*
* @param radius Camera radius
* @return this
*/
public UI3dContext setRadius(float radius) {
this.camera.radius.setValue(radius);
return this;
}
/**
* Sets perspective angle of the camera in degrees
*
* @param perspective Angle in degrees
* @return this
*/
public UI3dContext setPerspective(float perspective) {
this.perspective.setValue(perspective);
return this;
}
/**
* Sets the camera's maximum zoom speed
*
* @param cameraVelocity Max units/per second radius may change by
* @return this
*/
public UI3dContext setCameraVelocity(float cameraVelocity) {
this.cameraVelocity.setValue(cameraVelocity);
return this;
}
/**
* Set's the camera's zoom acceleration, 0 is infinite
*
* @param cameraAcceleration Acceleration for camera
* @return this
*/
public UI3dContext setCameraAcceleration(float cameraAcceleration) {
this.cameraAcceleration.setValue(cameraAcceleration);
return this;
}
/**
* Sets the camera's maximum rotation speed
*
* @param rotationVelocity Max radians/per second viewing angle may change by
* @return this
*/
public UI3dContext setRotationVelocity(float rotationVelocity) {
this.rotationVelocity.setValue(rotationVelocity);
return this;
}
/**
* Set's the camera's rotational acceleration, 0 is infinite
*
* @param rotationAcceleration Acceleration of camera rotation
* @return this
*/
public UI3dContext setRotationAcceleration(float rotationAcceleration) {
this.rotationAcceleration.setValue(rotationAcceleration);
return this;
}
/**
* Set the theta angle of viewing
*
* @param theta Angle about the y axis
* @return this
*/
public UI3dContext setTheta(double theta) {
this.camera.theta.setValue(theta);
return this;
}
/**
* Set the phi angle of viewing
*
* @param phi Angle about the y axis
* @return this
*/
public UI3dContext setPhi(float phi) {
this.camera.phi.setValue(phi);
return this;
}
/**
* Sets bounds on the radius
*
* @param minRadius Minimum camera radius
* @param maxRadius Maximum camera radius
* @return this
*/
public UI3dContext setRadiusBounds(float minRadius, float maxRadius) {
this.minRadius = minRadius;
this.maxRadius = maxRadius;
setRadius(LXUtils.constrainf(this.camera.radius.getValuef(), minRadius, maxRadius));
return this;
}
/**
* Set minimum radius
*
* @param minRadius Minimum camera radius
* @return this
*/
public UI3dContext setMinRadius(float minRadius) {
return setRadiusBounds(minRadius, this.maxRadius);
}
/**
* Set maximum radius
*
* @param maxRadius Maximum camera radius
* @return this
*/
public UI3dContext setMaxRadius(float maxRadius) {
return setRadiusBounds(this.minRadius, maxRadius);
}
/**
* Sets the center of the scene, only respected in ZOOM mode
*
* @param x X-coordinate
* @param y Y-coordinate
* @param z Z-coordinate
* @return this
*/
public UI3dContext setCenter(float x, float y, float z) {
this.camera.x.setValue(this.center.x = x);
this.camera.y.setValue(this.center.y = y);
this.camera.z.setValue(this.center.z = z);
return this;
}
/**
* Gets the center position of the scene
*
* @return center of scene
*/
public Vector3f getCenter() {
return this.center;
}
/**
* Gets the latest computed eye position
*
* @return eye position
*/
public Vector3f getEye() {
return this.eye;
}
private final LXParameter.MultiMonitor cameraMonitor =
new LXParameter.MultiMonitor(
this.radiusDamped, this.thetaDamped, this.phiDamped,
this.xDamped, this.yDamped, this.zDamped
);
private void computeCamera(boolean initialize) {
if (this.animating.isRunning() || this.animating.finished()) {
this.camera.lerp(this.cameraFrom, this.cameraTo, this.animating.getBasis());
}
final float rv = this.radiusDamped.getValuef();
final double tv = Math.toRadians(this.thetaDamped.getValue());
final double pv = Math.toRadians(this.phiDamped.getValue());
float sintheta = (float) Math.sin(tv);
float costheta = (float) Math.cos(tv);
float sinphi = (float) Math.sin(pv);
float cosphi = (float) Math.cos(pv);
float px = this.xDamped.getValuef();
float py = this.yDamped.getValuef();
float pz = this.zDamped.getValuef();
this.centerDamped.set(px, py, pz);
if (initialize) {
this.center.set(this.centerDamped);
}
this.eyeDamped.set(
px + rv * cosphi * sintheta,
py + rv * sinphi,
pz - rv * cosphi * costheta
);
this.eye.set(this.eyeDamped);
}
private boolean needsClear = false;
public final void draw(UI ui, View view) {
if (view != this.view) {
throw new IllegalArgumentException("Not currently supported to draw a 3dContext into a different view");
}
// If the view has drawn before and is now invisible,
// need to touch to clear it
if (!isVisible()) {
if (this.needsClear) {
this.view.bind();
BGFX.bgfx_touch(this.view.getId());
this.needsClear = false;
}
return;
}
this.needsClear = true;
// Set the camera view matrix
computeCamera(false);
this.view.setCamera(this.eyeDamped, this.centerDamped, this.up);
// Set projection matrix
float radiusValue = this.radiusDamped.getValuef();
switch (this.projection.getEnum()) {
case PERSPECTIVE:
float depthFactor = (float) Math.pow(10, this.depth.getValue());
this.view.setPerspective(
(float) Math.toRadians(this.perspective.getValuef()),
getWidth() / getHeight(),
radiusValue / depthFactor,
radiusValue * depthFactor
);
break;
case ORTHOGRAPHIC:
float halfRadiusWidth = radiusValue * .5f;
float halfRadiusHeight = halfRadiusWidth * getHeight() / getWidth();
this.view.setOrthographic(-halfRadiusWidth, halfRadiusWidth, -halfRadiusHeight, halfRadiusHeight, 0, radiusValue * 10);
break;
}
// Bind the view, touch it to make sure it's cleared in case no children draw
this.view.bind();
BGFX.bgfx_touch(this.view.getId());
// Check if view has changed
if (this.cameraMonitor.changed()) {
for (UIObject child : this.mutableChildren) {
((UI3dComponent) child).onCameraChanged(ui, this);
}
}
// Draw all the components in the scene
for (UIObject child : this.mutableChildren) {
((UI3dComponent) child).draw(ui, this.view);
}
}
public Matrix4f getViewMatrix() {
return this.view.getViewMatrix();
}
public Matrix4f getProjectionMatrix() {
return this.view.getProjectionMatrix();
}
@Override
protected void onMousePressed(MouseEvent mouseEvent, float mx, float my) {
super.onMousePressed(mouseEvent, mx, my);
if (mouseEvent.getCount() > 1) {
focus(mouseEvent);
}
}
private void updateFocusedCamera() {
this.focusCamera.getObject().set(this.camera, false);
this.animating.stop();
}
private enum MouseInteraction {
ROTATE_VIEW,
ROTATE_OBJECT,
ZOOM,
TRANSLATE_XY,
TRANSLATE_Z,
}
private MouseInteraction getInteraction(MouseEvent mouseEvent) {
switch (this.mouseMode.getEnum()) {
case OBJECT:
if (mouseEvent.isShiftDown()) {
if (mouseEvent.isMetaDown() || mouseEvent.isControlDown()) {
return MouseInteraction.ROTATE_VIEW;
}
return MouseInteraction.TRANSLATE_Z;
} else if (mouseEvent.isMetaDown() || mouseEvent.isControlDown()) {
return MouseInteraction.ROTATE_OBJECT;
}
return MouseInteraction.TRANSLATE_XY;
default:
case VIEW:
if (mouseEvent.isShiftDown()) {
return MouseInteraction.ZOOM;
} else if (mouseEvent.isMetaDown() || mouseEvent.isControlDown()) {
return MouseInteraction.TRANSLATE_XY;
}
return MouseInteraction.ROTATE_VIEW;
}
}
@Override
protected void onMouseDragged(MouseEvent mouseEvent, float mx, float my, float dx, float dy) {
MouseInteraction interaction = getInteraction(mouseEvent);
switch (interaction) {
case ROTATE_VIEW:
case ROTATE_OBJECT:
// NOTE: this is counter-intuitive but the rotation in the theta plane is divided relative
// to height, as we're almost always in a non-square aspect ratio and want horizontal rotation
// to feel consistent with vertical, in terms of same number of pixels mouse-movement should
// yield same number of degrees rotation independent of the plane
float rt = -dx / getHeight() * 1.5f * 180f;
float rp = dy / getHeight() * 1.5f * 180f;
if (interaction == MouseInteraction.ROTATE_VIEW) {
this.camera.theta.incrementValue(rt);
this.camera.phi.incrementValue(rp);
updateFocusedCamera();
} else {
for (MovementListener listener : this.movementListeners) {
listener.rotate(rt, rp);
}
}
break;
case ZOOM:
this.camera.radius.incrementValue(dy * 2.f / getHeight() * this.camera.radius.getValue());
updateFocusedCamera();
break;
case TRANSLATE_XY:
case TRANSLATE_Z:
final double thetaDampedRadians = Math.toRadians(this.thetaDamped.getValue());
final double phiDampedRadians = Math.toRadians(this.phiDamped.getValue());
final float tanPerspective = (float) Math.tan(.5 * Math.toRadians(this.perspective.getValue()));
float sinTheta = (float) Math.sin(thetaDampedRadians);
float cosTheta = (float) Math.cos(thetaDampedRadians);
float sinPhi = (float) Math.sin(phiDampedRadians);
float cosPhi = (float) Math.cos(phiDampedRadians);
// NOTE: this is counter-intuitive but don't be fooled, the dcx value is intentionally
// divided by the height, not the width, because aspect ratio is factored into perspective
final float radiusDamped = this.radiusDamped.getValuef();
float dcx = dx * 2.f / getHeight() * radiusDamped * tanPerspective;
float dcy = dy * 2.f / getHeight() * radiusDamped * tanPerspective;
float tx = 0, ty = 0, tz = 0;
if (interaction == MouseInteraction.TRANSLATE_XY) {
// Horizontal mouse movement goes "left and right" on screen
// Vertical mouse movement goes "up and down" on screen
tx = -dcx * cosTheta - dcy * sinTheta * sinPhi;
ty = dcy * cosPhi;
tz = -dcx * sinTheta + dcy * cosTheta * sinPhi;
} else if (interaction == MouseInteraction.TRANSLATE_Z) {
// Horizontal mouse movement is ignored
// Vertical mouse movement goes "in and out" of screen
tx = -dcy * sinTheta * cosPhi;
ty = -dcy * sinPhi;
tz = dcy * cosTheta * cosPhi;
}
if (this.mouseMode.getEnum() == MouseMode.VIEW) {
this.camera.x.incrementValue(tx);
this.camera.y.incrementValue(ty);
this.camera.z.incrementValue(tz);
updateFocusedCamera();
} else {
for (MovementListener listener : this.movementListeners) {
listener.translate(tx, ty, tz);
}
}
break;
}
}
@Override
protected void onMouseScroll(MouseEvent mouseEvent, float mx, float my, float dx, float dy) {
float multiplier = mouseEvent.isShiftDown() ? 3 : 1;
this.camera.radius.incrementValue(multiplier * -dy / getHeight() * this.camera.radius.getValue());
updateFocusedCamera();
}
@Override
protected void onKeyPressed(KeyEvent keyEvent, char keyChar, int keyCode) {
float degrees = 1;
if (keyEvent.isShiftDown()) {
degrees *= 10.f;
}
if (keyCode == KeyEvent.VK_LEFT) {
keyEvent.consume();
this.camera.theta.incrementValue(degrees);
updateFocusedCamera();
} else if (keyCode == KeyEvent.VK_RIGHT) {
keyEvent.consume();
this.camera.theta.incrementValue(-degrees);
updateFocusedCamera();
} else if (keyCode == KeyEvent.VK_UP) {
keyEvent.consume();
this.camera.phi.incrementValue(-degrees);
updateFocusedCamera();
} else if (keyCode == KeyEvent.VK_DOWN) {
keyEvent.consume();
this.camera.phi.incrementValue(degrees);
updateFocusedCamera();
}
}
private static final String KEY_ANIMATION = "animation";
private static final String KEY_ANIMATION_TIME = "animationTime";
private static final String KEY_CAMERA = "camera";
private static final String KEY_CUE = "cue";
private static final String KEY_FOCUS = "focus";
private static final String KEY_PROJECTION = "projection";
private static final String KEY_PERSPECTIVE = "perspective";
private static final String KEY_DEPTH = "depth";
@Override
public void save(LX lx, JsonObject object) {
object.addProperty(KEY_ANIMATION, this.animation.isOn());
object.addProperty(KEY_ANIMATION_TIME, this.animationTime.getValue());
object.addProperty(KEY_PROJECTION, this.projection.getValuei());
object.addProperty(KEY_PERSPECTIVE, this.perspective.getValue());
object.addProperty(KEY_DEPTH, this.depth.getValue());
object.add(KEY_CAMERA, LXSerializable.Utils.toObject(lx, this.camera));
object.add(KEY_CUE, LXSerializable.Utils.toArray(lx, this.cue));
object.addProperty(KEY_FOCUS, this.focusCamera.getValuei());
}
@Override
public void load(LX lx, JsonObject object) {
// Stop animation
this.animating.stop();
this.animation.setValue(false);
LXSerializable.Utils.loadDouble(this.animationTime, object, KEY_ANIMATION_TIME);
LXSerializable.Utils.loadInt(this.projection, object, KEY_PROJECTION);
LXSerializable.Utils.loadDouble(this.perspective, object, KEY_PERSPECTIVE);
LXSerializable.Utils.loadDouble(this.depth, object, KEY_DEPTH);
if (object.has(KEY_CAMERA)) {
LXSerializable.Utils.loadObject(lx, this.camera, object, KEY_CAMERA);
} else {
this.camera.reset();
}
LXSerializable.Utils.loadArray(lx, this.cue, object, KEY_CUE);
LXSerializable.Utils.loadInt(this.focusCamera, object, KEY_FOCUS);
// Updated damped values from loading
this.radiusDamped.setValue(this.camera.radius.getValue());
this.thetaDamped.setValue(this.camera.theta.getValue());
this.phiDamped.setValue(this.camera.phi.getValue());
this.xDamped.setValue(this.camera.x.getValue());
this.yDamped.setValue(this.camera.y.getValue());
this.zDamped.setValue(this.camera.z.getValue());
// Re-initialize position
computeCamera(true);
// Load animation setting
LXSerializable.Utils.loadBoolean(this.animation, object, KEY_ANIMATION);
}
}