/*
* 3D Canvas for GCode Visualizer.
*
* Created on Jan 29, 2013
*/
/*
Copyright 2013-2017 Will Winder
This file is part of Universal Gcode Sender (UGS).
UGS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
UGS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with UGS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.willwinder.ugs.nbm.visualizer.shared;
import com.jogamp.opengl.GL;
import static com.jogamp.opengl.GL.GL_DEPTH_TEST;
import static com.jogamp.opengl.GL.GL_FRONT_AND_BACK;
import static com.jogamp.opengl.GL.GL_LINES;
import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLDrawable;
import com.jogamp.opengl.GLEventListener;
import static com.jogamp.opengl.fixedfunc.GLLightingFunc.GL_AMBIENT_AND_DIFFUSE;
import static com.jogamp.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW;
import static com.jogamp.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION;
import com.jogamp.opengl.glu.GLU;
import com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions;
import com.willwinder.ugs.nbm.visualizer.renderables.Grid;
import com.willwinder.ugs.nbm.visualizer.renderables.MouseOver;
import com.willwinder.ugs.nbm.visualizer.renderables.OrientationCube;
import com.willwinder.ugs.nbm.visualizer.renderables.Tool;
import com.willwinder.universalgcodesender.model.Position;
import com.willwinder.universalgcodesender.model.UnitUtils;
import com.willwinder.universalgcodesender.uielements.helpers.FPSCounter;
import com.willwinder.universalgcodesender.uielements.helpers.Overlay;
import com.willwinder.universalgcodesender.visualizer.MouseProjectionUtils;
import com.willwinder.universalgcodesender.visualizer.VisualizerUtils;
import java.awt.Color;
import java.awt.Font;
import java.awt.Point;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import org.openide.util.lookup.ServiceProvider;
import org.openide.util.lookup.ServiceProviders;
/**
*
* @author wwinder
*
*/
@SuppressWarnings("serial")
@ServiceProviders(value = {
@ServiceProvider(service = IRenderableRegistrationService.class),
@ServiceProvider(service = IRendererNotifier.class),
@ServiceProvider(service = GcodeRenderer.class)})
public class GcodeRenderer implements GLEventListener, IRenderableRegistrationService, IRendererNotifier {
private static final Logger logger = Logger.getLogger(GcodeRenderer.class.getName());
private static boolean ortho = true;
private static double orthoRotation = -45;
private static boolean forceOldStyle = false;
private static boolean debugCoordinates = false; // turn on coordinate debug output
// Machine data
private final Point3d machineCoord;
private final Point3d workCoord;
// GL Utility
private GLU glu;
private GLAutoDrawable drawable = null;
// Projection variables
private Point3d center, eye;
private Point3d objectMin, objectMax;
private double maxSide;
private int xSize, ySize;
private double minArcLength;
private double arcLength;
// Scaling
private double scaleFactor = 1;
private double scaleFactorBase = 1;
private double zoomMultiplier = 1;
private final boolean invertZoom = false; // TODO: Make configurable
// const values until added to settings
private final double minZoomMultiplier = 1;
private final double maxZoomMultiplier = 30;
private final double zoomIncrement = 0.2;
// Movement
private final int panMouseButton = InputEvent.BUTTON2_MASK; // TODO: Make configurable
private double panMultiplierX = 1;
private double panMultiplierY = 1;
private Vector3d translationVectorH;
private Vector3d translationVectorV;
// Mouse rotation data
private Point mouseLastWindow;
private Point mouseCurrentWindow;
private Point3d mouseWorldXY;
private Point3d rotation;
private FPSCounter fpsCounter;
private Overlay overlay;
private final String dimensionsLabel = "";
private final ArrayList<Renderable> objects;
private boolean idle = true;
// Preferences
private java.awt.Color clearColor;
/**
* Constructor.
*/
public GcodeRenderer() {
eye = new Point3d(0, 0, 1.5);
center = new Point3d(0, 0, 0);
objectMin = new Point3d(-10,-10,-10);
objectMax = new Point3d( 10, 10, 10);
workCoord = new Point3d(0, 0, 0);
machineCoord = new Point3d(0, 0, 0);
rotation = new Point3d(0.0, -30.0, 0.0);
setVerticalTranslationVector();
setHorizontalTranslationVector();
objects = new ArrayList<>();
objects.add(new Tool());
objects.add(new MouseOver());
objects.add(new OrientationCube(0.5f));
objects.add(new Grid());
Collections.sort(objects);
reloadPreferences();
}
@Override
public void registerRenderable(Renderable r) {
objects.add(r);
Collections.sort(objects);
}
@Override
public void removeRenderable(Renderable r) {
objects.remove(r);
Collections.sort(objects);
}
public void forceRedraw() {
if (drawable != null) {
drawable.display();
}
}
/**
* Get the location on the XY plane of the mouse.
*/
public Point3d getMouseWorldLocation() {
return this.mouseWorldXY;
}
public void setWorkCoordinate(Position p) {
this.workCoord.set(p.getPositionIn(UnitUtils.Units.MM));
}
public void setMachineCoordinate(Position p) {
this.machineCoord.set(p.getPositionIn(UnitUtils.Units.MM));
}
final public void reloadPreferences() {
VisualizerOptions vo = new VisualizerOptions();
clearColor = (Color)vo.getOptionForKey("platform.visualizer.color.background").value;
for (Renderable r : objects) {
r.reloadPreferences(vo);
}
forceRedraw();
}
// ------ Implement methods declared in GLEventListener ------
/**
* Called back immediately after the OpenGL context is initialized. Can be used
* to perform one-time initialization. Run only once.
* GLEventListener method.
*/
@Override
public void init(GLAutoDrawable drawable) {
logger.log(Level.INFO, "Initializing OpenGL context.");
this.drawable = drawable;
// TODO: Figure out scale factor / dimensions label based on GcodeRenderer
/*
this.scaleFactorBase = VisualizerUtils.findScaleFactor(this.xSize, this.ySize, this.objectMin, this.objectMax);
this.scaleFactor = this.scaleFactorBase * this.zoomMultiplier;
double objectWidth = this.objectMax.x-this.objectMin.x;
double objectHeight = this.objectMax.y-this.objectMin.y;
this.dimensionsLabel = Localization.getString("VisualizerCanvas.dimensions") + ": "
+ Localization.getString("VisualizerCanvas.width") + "=" + format.format(objectWidth) + " "
+ Localization.getString("VisualizerCanvas.height") + "=" + format.format(objectHeight);
*/
this.fpsCounter = new FPSCounter((GLDrawable) drawable, new Font("SansSerif", Font.BOLD, 12));
this.overlay = new Overlay((GLDrawable) drawable, new Font("SansSerif", Font.BOLD, 12));
this.overlay.setColor(127, 127, 127, 100);
this.overlay.setTextLocation(Overlay.LOWER_LEFT);
// Parse random gcode file and generate something to draw.
GL2 gl = drawable.getGL().getGL2(); // get the OpenGL graphics context
glu = new GLU(); // get GL Utilities
gl.glClearColor(clearColor.getRed()/255f, clearColor.getGreen()/255f, clearColor.getBlue()/255f, clearColor.getAlpha()/255f);
gl.glClearDepth(1.0f); // set clear depth value to farthest
gl.glDepthFunc(GL2.GL_LEQUAL); // the type of depth test to do
gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL2.GL_NICEST); // best perspective correction
gl.glShadeModel(GL2.GL_SMOOTH); // blends colors nicely, and smoothes out lighting
gl.glEnable(GL2.GL_BLEND);
gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA);
gl.glLoadIdentity();
for (Renderable r : objects) {
r.init(drawable);
}
}
/**
* Call-back handler for window re-size event. Also called when the drawable is
* first set to visible.
* GLEventListener method.
*/
@Override
public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
//logger.log(Level.INFO, "Reshaping OpenGL context.");
this.xSize = width;
this.ySize = height;
GL2 gl = drawable.getGL().getGL2(); // get the OpenGL 2 graphics context
// Set the view port (display area) to cover the entire window
gl.glViewport(0, 0, xSize, ySize);
resizeForCamera(objectMin, objectMax, 0.9);
}
public void setObjectSize(Point3d min, Point3d max) {
this.objectMin = min;
this.objectMax = max;
idle = false;
resizeForCamera(objectMin, objectMax, 0.9);
forceRedraw();
}
/**
* Zoom the visualizer to the given region.
*/
public void zoomToRegion(Point3d min, Point3d max, double bufferFactor) {
if (min == null || max == null) return;
if (this.ySize == 0){ this.ySize = 1; } // prevent divide by zero
// Figure out offset compared to the current center.
Point3d regionCenter = VisualizerUtils.findCenter(min, max);
this.eye.x = regionCenter.x - this.center.x;
this.eye.y = regionCenter.y - this.center.y;
// Figure out what the scale factors would be if we reset this object.
double _scaleFactorBase = VisualizerUtils.findScaleFactor(this.xSize, this.ySize, min, max, bufferFactor);
double _scaleFactor = _scaleFactorBase * this.zoomMultiplier;
// Calculate the zoomMultiplier needed to get to that scale, and set it.
this.zoomMultiplier = _scaleFactor/this.scaleFactorBase;
this.scaleFactor = this.scaleFactorBase * this.zoomMultiplier;
}
/**
* Zoom to display the given region leaving the suggested buffer.
*/
private void resizeForCamera(Point3d min, Point3d max, double bufferFactor) {
if (min == null || max == null) return;
if (this.ySize == 0){ this.ySize = 1; } // prevent divide by zero
this.center = VisualizerUtils.findCenter(min, max);
this.scaleFactorBase = VisualizerUtils.findScaleFactor(this.xSize, this.ySize, min, max, bufferFactor);
this.scaleFactor = this.scaleFactorBase * this.zoomMultiplier;
this.panMultiplierX = VisualizerUtils.getRelativeMovementMultiplier(min.x, max.x, this.xSize);
this.panMultiplierY = VisualizerUtils.getRelativeMovementMultiplier(min.y, max.y, this.ySize);
}
/**
* Called back by the animator to perform rendering.
* GLEventListener method.
*/
@Override
public void display(GLAutoDrawable drawable) {
this.setupPerpective(this.xSize, this.ySize, drawable, ortho);
final GL2 gl = drawable.getGL().getGL2();
gl.glClearColor(clearColor.getRed()/255f, clearColor.getGreen()/255f, clearColor.getBlue()/255f, clearColor.getAlpha()/255f);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
gl.glEnable(GL2.GL_LIGHT0);
gl.glEnable(GL2.GL_NORMALIZE);
gl.glEnable (GL2.GL_COLOR_MATERIAL ) ;
float ambient[] = { 0.4f, 0.4f, 0.4f, 1.0f };
float diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f };
float specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
float position[] = { 0f, 0f, 50f, 1.0f };
float lmodel_ambient[] = { 1.0f, 1.0f, 1.0f, 1.0f };
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_SPECULAR, specular, 0);
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_SPECULAR, ambient, 0);
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_DIFFUSE, diffuse, 0);
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_POSITION, position, 0);
gl.glLightModelfv(GL2.GL_LIGHT_MODEL_AMBIENT, lmodel_ambient, 0);
gl.glEnable(GL_DEPTH_TEST);
// Setup the current matrix so that the projection can be done.
if (mouseLastWindow != null) {
gl.glPushMatrix();
gl.glRotated(this.rotation.x, 0.0, 1.0, 0.0);
gl.glRotated(this.rotation.y, 1.0, 0.0, 0.0);
gl.glTranslated(-this.eye.x - this.center.x, -this.eye.y - this.center.y, -this.eye.z - this.center.z);
this.mouseWorldXY = MouseProjectionUtils.intersectPointWithXYPlane(
drawable, mouseLastWindow.x, mouseLastWindow.y);
gl.glPopMatrix();
} else {
this.mouseWorldXY = new Point3d(0, 0, 0);
}
// Render the different parts of the scene.
for (Renderable r : objects) {
gl.glPushMatrix();
if (r.rotate()) {
gl.glRotated(this.rotation.x, 0.0, 1.0, 0.0);
gl.glRotated(this.rotation.y, 1.0, 0.0, 0.0);
}
if (r.center()) {
gl.glTranslated(-this.eye.x - this.center.x, -this.eye.y - this.center.y, -this.eye.z - this.center.z);
}
r.draw(drawable, idle, workCoord, objectMin, objectMax, scaleFactor, mouseWorldXY, rotation);
gl.glPopMatrix();
}
this.fpsCounter.draw();
this.overlay.draw(this.dimensionsLabel);
gl.glLoadIdentity();
update();
}
private void renderCornerAxes(GLAutoDrawable drawable) {
final GL2 gl = drawable.getGL().getGL2();
gl.glLineWidth(4);
float ar = (float)xSize/ySize;
float size = 0.1f;
float size2 = size/2;
float fromEdge = 0.8f;
gl.glPushMatrix();
gl.glTranslated(-0.51*ar*fromEdge, 0.51*fromEdge, -0.5f);
gl.glRotated(this.rotation.x, 0.0, 1.0, 0.0);
gl.glRotated(this.rotation.y, 1.0, 0.0, 0.0);
gl.glBegin(GL_LINES);
// X-Axis
gl.glColor3f( 1, 0, 0 );
gl.glVertex3f( 0, 0f, 0f );
gl.glVertex3f( 1, 0f, 0f );
// Y-Axis
gl.glColor3f( 0, 1, 0 );
gl.glVertex3f( 0, 0f, 0f );
gl.glVertex3f( 0, 1f, 0f );
// Z-Axis
gl.glColor3f( 0, 1, 1 );
gl.glVertex3f( 0, 0f, 0f );
gl.glVertex3f( 0, 0f, 1f );
gl.glEnd();
gl.glPopMatrix();
//# Draw number 50 on x/y-axis line.
//glRasterPos2f(50,-5)
//glutInit()
//A = 53
//glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, A)
//glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, 48)
//glRasterPos2f(-5,50)
//glutInit()
//A = 53
//glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, A)
//glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, 48)
}
/**
* Setup the perspective matrix.
*/
private void setupPerpective(int x, int y, GLAutoDrawable drawable, boolean ortho) {
final GL2 gl = drawable.getGL().getGL2();
float aspectRatio = (float)x / y;
if (ortho) {
gl.glMatrixMode(GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(-0.60*aspectRatio/scaleFactor,0.60*aspectRatio/scaleFactor,-0.60/scaleFactor,0.60/scaleFactor,
-10/scaleFactor,10/scaleFactor);
gl.glMatrixMode(GL_MODELVIEW);
gl.glLoadIdentity();
} else {
// Setup perspective projection, with aspect ratio matches viewport
gl.glMatrixMode(GL_PROJECTION); // choose projection matrix
gl.glLoadIdentity(); // reset projection matrix
glu.gluPerspective(45.0, aspectRatio, 0.1, 20000.0); // fovy, aspect, zNear, zFar
// Move camera out and point it at the origin
glu.gluLookAt(this.eye.x, this.eye.y, this.eye.z,
0, 0, 0,
0, 1, 0);
// Enable the model-view transform
gl.glMatrixMode(GL_MODELVIEW);
gl.glLoadIdentity(); // reset
}
}
/**
* Called after each render.
*/
private void update() {
if (debugCoordinates) {
System.out.println("Machine coordinates: " + this.machineCoord.toString());
System.out.println("Work coordinates: " + this.workCoord.toString());
System.out.println("-----------------");
}
}
/**
* Called back before the OpenGL context is destroyed.
* Release resource such as buffers.
* GLEventListener method.
*/
@Override
synchronized public void dispose(GLAutoDrawable drawable) {
logger.log(Level.INFO, "Disposing OpenGL context.");
}
private void setHorizontalTranslationVector() {
double x = Math.cos(Math.toRadians(this.rotation.x));
double xz = Math.sin(Math.toRadians(this.rotation.x));
double y = xz * Math.sin(Math.toRadians(this.rotation.y));
double yz = xz * Math.cos(Math.toRadians(this.rotation.y));
translationVectorH = new Vector3d(x, y, yz);
translationVectorH.normalize();
}
private void setVerticalTranslationVector(){
double y = Math.cos(Math.toRadians(this.rotation.y));
double yz = Math.sin(Math.toRadians(this.rotation.y));
translationVectorV = new Vector3d(0, y, yz);
translationVectorV.normalize();
}
public void mouseMoved(Point lastPoint) {
mouseLastWindow = lastPoint;
}
public void mouseRotate(Point point) {
this.mouseCurrentWindow = point;
if (this.mouseLastWindow != null) {
int dx = this.mouseCurrentWindow.x - this.mouseLastWindow.x;
int dy = this.mouseCurrentWindow.y - this.mouseLastWindow.y;
rotation.x = this.rotation.x += dx / 2.0;
rotation.y = Math.min(0, Math.max(-180, this.rotation.y += dy / 2.0));
if (ortho) {
setHorizontalTranslationVector();
setVerticalTranslationVector();
}
}
// Now that the motion has been accumulated, reset last.
this.mouseLastWindow = this.mouseCurrentWindow;
}
public void mousePan(Point point) {
this.mouseCurrentWindow = point;
int dx = this.mouseCurrentWindow.x - this.mouseLastWindow.x;
int dy = this.mouseCurrentWindow.y - this.mouseLastWindow.y;
pan(dx, dy);
}
public void pan(int dx, int dy) {
if (ortho) {
// Treat dx and dy as vectors relative to the rotation angle.
this.eye.x -= ((dx * this.translationVectorH.x * this.panMultiplierX) + (dy * this.translationVectorV.x * panMultiplierY));
this.eye.y += ((dy * this.translationVectorV.y * panMultiplierY) - (dx * this.translationVectorH.y * this.panMultiplierX));
this.eye.z -= ((dx * this.translationVectorH.z * this.panMultiplierX) + (dy * this.translationVectorV.z * panMultiplierY));
} else {
this.eye.x += dx;
this.eye.y += dy;
}
// Now that the motion has been accumulated, reset last.
this.mouseLastWindow = this.mouseCurrentWindow;
}
public void zoom(int delta) {
if (delta == 0)
return;
if (delta > 0) {
if (this.invertZoom)
zoomOut(delta);
else
zoomIn(delta);
} else if (delta < 0) {
if (this.invertZoom)
zoomIn(delta * -1);
else
zoomOut(delta * -1);
}
}
private void zoomOut(int increments) {
if (ortho) {
if (this.zoomMultiplier <= this.minZoomMultiplier)
return;
this.zoomMultiplier -= increments * zoomIncrement;
if (this.zoomMultiplier < this.minZoomMultiplier)
this.zoomMultiplier = this.minZoomMultiplier;
this.scaleFactor = this.scaleFactorBase * this.zoomMultiplier;
} else {
this.eye.z += increments;
}
}
private void zoomIn(int increments) {
if (ortho) {
if (this.zoomMultiplier >= this.maxZoomMultiplier)
return;
this.zoomMultiplier += increments * zoomIncrement;
if (this.zoomMultiplier > this.maxZoomMultiplier)
this.zoomMultiplier = this.maxZoomMultiplier;
this.scaleFactor = this.scaleFactorBase * this.zoomMultiplier;
} else {
this.eye.z -= increments;
}
}
/**
* Reset the view angle and zoom.
*/
public void resetView() {
this.zoomMultiplier = 1;
this.scaleFactor = this.scaleFactorBase;
this.eye.x = 0;
this.eye.y = 0;
this.eye.z = 1.5;
this.rotation.x = 0;
this.rotation.y = -30;
this.rotation.z = 0;
}
}