Sorry for the delay. I hardly ever check my gmail account. I have too many accounts!
That code is from a Java Application I wrote to play around with the delta stuff. The code is below.
I don't know exactly what the code does now. It has a bunch of modes of operation. I was playing around with seeing how all sorts of different errors affected the accuracy of a delta. This code was not written for public consumption so I would hard code various modes (I think I have UI placeholders for maybe someday making them changeable from the UI). The dCol variables are globals that could be set to various values representing the column positions. I have moved them into any number of configurations. The standard delta configuration based on the dRadius value is generated by the calcValues function.
package com.highergraphics.delta3d;
/**
* This app will show a color topography map of the X-Y error for a delta CNC router/3D printer.
* The error will be calculated by varying each carriage around its ideal value and finding the maximum error.
* All combinations of carriage variations will be tried.
*
* The inputs to the system are column spacing, effector arm length, carriage error value.
* We were assuming a zero offset, which is the same as using a "virtual column position".
* But now we have added Effector platform offset and carriage offset as inputs. Now they
* are used to calculate the virtual column position and the column spacing input is
* the real column positions.
*
*/
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingWorker;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.FlowLayout;
import javax.swing.JTextField;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import javax.swing.JLabel;
import javax.swing.JRadioButton;
import javax.swing.border.EtchedBorder;
import javax.swing.ButtonGroup;
import javax.swing.JProgressBar;
public class HGdelta3DErrApp {
private JFrame frame;
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
HGdelta3DErrApp window = new HGdelta3DErrApp();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the application.
*/
public HGdelta3DErrApp() {
initialize();
readIni();
}
/**
* Initialize the contents of the frame.
*/
@SuppressWarnings("serial")
private void initialize() {
frame = new JFrame();
frame.setBounds(100, 100, 483, 431);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new BorderLayout(0, 0));
JMenuBar menuBar = new JMenuBar();
frame.getContentPane().add(menuBar, BorderLayout.NORTH);
JMenu mnFile = new JMenu("File");
menuBar.add(mnFile);
JMenuItem mntmSaveAs = new JMenuItem("Save As...");
mntmSaveAs.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
saveFile();
}
});
mnFile.add(mntmSaveAs);
JPanel pnlProgress = new JPanel();
frame.getContentPane().add(pnlProgress, BorderLayout.SOUTH);
GridBagLayout gbl_pnlProgress = new GridBagLayout();
gbl_pnlProgress.columnWidths = new int[]{0, 160, 146, 0};
gbl_pnlProgress.rowHeights = new int[]{14, 0};
gbl_pnlProgress.columnWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
gbl_pnlProgress.rowWeights = new double[]{0.0, Double.MIN_VALUE};
pnlProgress.setLayout(gbl_pnlProgress);
lblProgress = new JLabel("1st pass calculating errors");
GridBagConstraints gbc_lblProgress = new GridBagConstraints();
gbc_lblProgress.insets = new Insets(0, 10, 0, 5);
gbc_lblProgress.gridx = 0;
gbc_lblProgress.gridy = 0;
pnlProgress.add(lblProgress, gbc_lblProgress);
progressBar = new JProgressBar();
GridBagConstraints gbc_progressBar = new GridBagConstraints();
gbc_progressBar.gridwidth = 2;
gbc_progressBar.fill = GridBagConstraints.HORIZONTAL;
gbc_progressBar.insets = new Insets(5, 20, 0, 0);
gbc_progressBar.gridx = 1;
gbc_progressBar.gridy = 0;
pnlProgress.add(progressBar, gbc_progressBar);
pnlImg = new JPanel(){
@Override
public void paintComponent(Graphics arg0) {
super.paintComponent(arg0);
if(theImage == null)return;
paintImg(arg0, pnlImg.getBounds());
}
};
frame.getContentPane().add(pnlImg, BorderLayout.CENTER);
pnlImg.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
calcValues();
frame.setBounds(0, 0, 800, 600);
hgCtlDiag.setBounds(0, 0, 550, 400);
hgCtlDiag.addComponentListener(new ComponentListener() {
@Override
public void componentShown(ComponentEvent e) {
}
@Override
public void componentResized(ComponentEvent e) {
}
@Override
public void componentMoved(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) {
System.exit(0);
}
});
hgCtlDiag.getBtnCalc().addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
hgCtlDiagCmd(); }
});
hgCtlDiag.setVisible(true);
}
HGdeltaErrCtrlDialog hgCtlDiag = new HGdeltaErrCtrlDialog();
boolean bUnitsMM = true;
double dRadius = 124;
double dColSpace = dRadius * Math.sqrt(3d);
double dArmLen = 175;
int iMode = 4;
double dTestStep = 0.5;
double dError = 0.01;
boolean bSingle = false;
void hgCtlDiagCmd(){
bUnitsMM = hgCtlDiag.areUnitsMM();
dRadius = hgCtlDiag.getRadius() - hgCtlDiag.getEffectorOffset() - hgCtlDiag.getCarriageOffset();
dColSpace = dRadius * Math.sqrt(3d);
dArmLen = hgCtlDiag.getArmLen();
dArmSqrd = dArmLen * dArmLen;
dError = hgCtlDiag.getCarriageError();
bSingle = hgCtlDiag.isSingleCarriage();
dTestStep = hgCtlDiag.getTestStep();
if(hgCtlDiag.getRdbtnErrorInX().isSelected()){
iMode = 0;
}else if(hgCtlDiag.getRdbtnErrorInY().isSelected()){
iMode = 1;
}else if(hgCtlDiag.getRdbtnErrorInZ().isSelected()){
iMode = 2;
}else if(hgCtlDiag.getRdbtnErrorInXY().isSelected()){
iMode = 3;
}else{
iMode = 4;
}
createImageTask();
}
void createImageTask(){
SwingWorker<Void, Void> sw = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
createImage();
return null;
}
};
sw.execute();
}
void createImage(){
double dRange = 2 * dArmLen - dColSpace;
if(dRange < 1.5 * dColSpace)dRange = 1.5 * dColSpace;
dErrArray = new double[(int)(dRange/dTestStep)][(int)(dRange/dTestStep)];
double dMinX = dRange;
double dMinY = dRange;
double dMaxX = -dRange;
double dMaxY = -dRange;
int iMinI = 0;
int iMaxI = 0;
int iMinJ = 0;
int iMaxJ = 0;
double dMinErr = 100;
double dMaxErr = 0;
lblProgress.setText("1st pass - calculating errors");
progressBar.setStringPainted(true);
progressBar.setMaximum(100);
progressBar.setValue(0);
int iCnt = 0;
int iMax = (int)(dRange/dTestStep);
for(int i=0; i < iMax; i++){
for(int j=0; j < iMax; j++){
double dTestX = i*dTestStep - dRange/2;
double dTestY = dRange/2 - j*dTestStep;
double dErr = calcError(new double[]{dTestX, dTestY, 0}, iMode);
dErrArray[i][j] = dErr;
if(dErr >= 0){
if(dTestX < dMinX){
iMinI = i;
dMinX = dTestX;
}
if(dTestY < dMinY){
iMaxJ = j;
dMinY = dTestY;
}
if(dTestX > dMaxX){
iMaxI = i;
dMaxX = dTestX;
}
if(dTestY > dMaxY){
iMinJ = j;
dMaxY = dTestY;
}
if(dErr < dMinErr)dMinErr = dErr;
if(dErr > dMaxErr)dMaxErr = dErr;
}
}
iCnt++;
progressBar.setValue((int) (100 * (double)iCnt/(double)iMax));
progressBar.repaint();
}
if(dMinY > dCol[0][1] - 1){
dMinY = dCol[0][1] - 1;
iMaxJ = (int) ((dRange/2 - dMinY)/dTestStep);
}
if(dMaxY < dCol[2][1] + 1){
dMaxY = dCol[2][1] + 1;
iMinJ = (int) ((dRange/2 - dMaxY)/dTestStep);
}
int iHeader = 90;
int iVBorder = 10;
int iHBorder = 10;
int iFooter = 50;
int iImgWidth = iMaxI - iMinI + 1;
int iImgHeight = iMaxJ - iMinJ + 1;
if(2 * iHBorder + iImgWidth < 540)iHBorder = (540 - iImgWidth)/2;
int iWidth = 2 * iHBorder + iImgWidth;
int iHeight = iHeader + 2 * iVBorder + iImgHeight + iFooter;
theImage = new BufferedImage(iWidth, iHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = theImage.createGraphics();
RenderingHints rh = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
rh.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
rh.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.white);
g2.fillRect(0, 0, iWidth, iHeight);
lblProgress.setText("2nd pass - generating image");
progressBar.setMaximum(100);
progressBar.setValue(0);
iMax = (iMaxI - iMinI) * (iMaxJ - iMinJ);
iCnt = 0;
for(int i=iMinI; i <= iMaxI; i++){
for(int j=iMinJ; j <= iMaxJ; j++){
double dErr = dErrArray[i][j];
Color colorPix = Color.white;
if(dErr >= 0){
dErr = (dErr - dMinErr)/(dMaxErr - dMinErr);
colorPix = getColor(dErr);
}
theImage.setRGB(i - iMinI + iHBorder, j - iMinJ + iHeader + iVBorder, colorPix.getRGB());
iCnt++;
progressBar.setValue((int) (100 * (double)iCnt/(double)iMax));
progressBar.repaint();
//pnlImg.repaint();
}
}
g2.setColor(Color.black);
for(int iIdx = 0; iIdx < 3; iIdx++){
int x = iHBorder + (int) ((dCol[iIdx][0] - dMinX)/dTestStep);
int y = iHeader + iVBorder + (int) ((dMaxY - dCol[iIdx][1])/dTestStep);
g2.fillOval(x - 10, y-10, 20, 20);
}
g2.drawString(sTitle, 250, 20);
String sUnits = "in.";
if(bUnitsMM)sUnits = "mm";
String sCol = String.format("Virtual Column Spacing - %.2f %s", new Object[]{dColSpace, sUnits});
g2.drawString(sCol, 20, 20);
String sRad = String.format("Virtual Column Radius - %.2f %s", new Object[]{dRadius, sUnits});
g2.drawString(sRad, 20, 35);
String sArm = String.format("Arm Length - %.2f %s", new Object[]{dArmLen, sUnits});
g2.drawString(sArm, 20, 50);
String sAcmeErr = String.format("Carriage Error - %.4f %s", new Object[]{dError, sUnits});
g2.drawString(sAcmeErr, 20, 65);
String sErrorMode = "Error Mode - ";
if(bSingle){
sErrorMode += "Single Column";
}else{
sErrorMode += "Multiple Columns";
}
g2.drawString(sErrorMode, 20, 80);
int iFooterTop = iHeader + 2 * iVBorder + iImgHeight;
for(int iIdx = 0; iIdx < 11; iIdx++){
g2.drawString(String.format("%.4f", new Object[]{dMaxErr - iIdx * 0.10 * (dMaxErr - dMinErr)}), 20 + iIdx * 45, iFooterTop + 15);
}
for(int iIdx = 0; iIdx < 10; iIdx++){
double dLegend = dMaxErr - (((double)iIdx * 0.10) + 0.05) * (dMaxErr - dMinErr);
g2.setColor(getColor((dLegend - dMinErr)/(dMaxErr - dMinErr)));
g2.fillRect(52 + iIdx * 45, iFooterTop + 20, 20, 20);
}
pnlImg.repaint();
}
Color getColor(double dErr){
int[] iMaxColor = {0, 0, 0};
int[] iMinColor = {0, 0, 0};
dErr = Math.floor(10*dErr)/10;
int[] iColor = {0, 0, 0};
if(dErr > 0.75){
iMaxColor = new int[]{255,0,0};
iMinColor = new int[]{255,255,0};
dErr -= 0.75;
}else if(dErr > 0.5){
iMaxColor = new int[]{255,255,0};
iMinColor = new int[]{0,255,255};
dErr -= 0.5;
}else if(dErr > 0.25){
iMaxColor = new int[]{0,255,255};
iMinColor = new int[]{0,0,255};
dErr -= 0.25;
}else{
iMaxColor = new int[]{0,0,255};
iMinColor = new int[]{255,0,255};
}
dErr *= 4;
for(int iIdx = 0; iIdx < 3; iIdx++){
iColor[iIdx] = iMinColor[iIdx] + (int)(dErr * (double)(iMaxColor[iIdx]- iMinColor[iIdx]));
}
return new Color(iColor[0], iColor[1], iColor[2]);
}
/**
* We are changing the way the image is laid out.
*/
//double dMaxErr = 0.0025;
double[][] dErrArray;
double[][] dCol = new double[3][3];
// double[] dColX = new double[3];
// double[] dColY = new double[3];
double dArmSqrd = dArmLen * dArmLen;
double dBaseZ = Math.sqrt(dArmSqrd - Math.pow(dRadius, 2));
/**
* Calculate the locations of the columns based on column spacing.
* The columns are equally spaced (120 degrees apart) on a circle
* centered at 0,0 with one column on the positive y axis.
*/
void calcValues(){
// dColX[0] = 0;
// dColY[0] = dRadius;
// dColX[1] = dRadius * Math.cos(Math.PI/6);
// dColY[1] = -dRadius * Math.cos(Math.PI/3);
// dColX[2] = -dColX[1];
// dColY[2] = dColY[1];
dCol[0][0] = -dRadius * Math.cos(Math.PI/6);
dCol[0][1] = -dRadius * Math.sin(Math.PI/6);
dCol[0][2] = 0;
dCol[1][0] = dRadius * Math.cos(Math.PI/6);
dCol[1][1] = -dRadius * Math.sin(Math.PI/6);
dCol[1][2] = 0;
dCol[2][0] = 0;
dCol[2][1] = dRadius;
dCol[2][2] = 0;
}
/**
* This calculates the error at a given x,y location. The possible modes are
* X error
* Y error
* X-Y error
* Total error
*
* @param dX
* @param dY
* @param iMode
* @return
*/
boolean bExit = true;
String sTitle = "";
String sMode = "";
double calcError(double[] dP, int iMode){
for(int iIdx = 0; iIdx < 3; iIdx++){
double dTemp = vectorDotProd(dCol[iIdx], dP);
if(dTemp >= dRadius * dRadius)return -1;
}
//First we calculate the exact values for each col Z
//double[] dP = {dX, dY, 0};
double[] dZ = new double[3];
for(int iIdx = 0; iIdx < 3; iIdx++){
//dZ[iIdx] = Math.sqrt(dArmSqrd - (Math.pow((dColX[iIdx] - dX), 2) + Math.pow((dColY[iIdx] - dY), 2)));
dZ[iIdx] = dArmSqrd - (Math.pow((dCol[iIdx][0] - dP[0]), 2) + Math.pow((dCol[iIdx][1] - dP[1]), 2));
if(dZ[iIdx] < 0) return -1;
dZ[iIdx] = Math.sqrt(dZ[iIdx]);
}
//if(bExit)return 0;
// System.out.println(String.format("DZ =>%.2f, %.2f, %.2f", new Object[]{dZ[0], dZ[1], dZ[2]}));
//Now add error to each d and find total error
double dErr = 0;
for(double i = -1; i < 2; i++){
for(double j = -1; j < 2; j++){
for(double k = -1; k < 2; k++){
if(i == 0 && j == 0 && k == 0)continue; //No error
if(bSingle){
//Only one column error at a time
if(i != 0 && j != 0)continue;
if(i != 0 && k != 0)continue;
if(j != 0 && k != 0)continue;
}
// double[] dErrPos2 = forwardKinematicstl(new double[]{dZ[0] + i*dError, dZ[1] + j*dError, dZ[2] + k*dError});
double[] dErrPos = forwardKinematics(new double[]{dZ[0] + i*dError, dZ[1] + j*dError, dZ[2] + k*dError});
// for(int iIdx = 0; iIdx < 3; iIdx++){
// double dDiff = Math.abs(dErrPos[iIdx] - dErrPos2[iIdx]);
// if(dDiff > 0.0001){
// System.out.println("Error of " + Double.toString(dDiff));
// }
// }
double dThisErr = 0;
switch(iMode){
case 0:
dThisErr = Math.abs(dP[0] - dErrPos[0]);
sMode = "X";
sTitle = "Max Error in X";
break;
case 1:
dThisErr = Math.abs(dP[1] - dErrPos[1]);
sMode = "Y";
sTitle = "Max Error in Y";
break;
case 2:
dThisErr = Math.abs(dP[2] - dErrPos[2]);
sMode = "Z";
sTitle = "Max Error in Z";
break;
case 3:
dThisErr = Math.sqrt(Math.pow(dP[0] - dErrPos[0], 2) + Math.pow(dP[1] - dErrPos[1], 2));
sMode = "X-Y";
sTitle = "Max Error in X-Y";
break;
case 4:
dThisErr = Math.sqrt(Math.pow(dP[0] - dErrPos[0], 2) + Math.pow(dP[1] - dErrPos[1], 2) + Math.pow(dErrPos[2], 2));
sMode = "X-Y-Z";
sTitle = "Max Error in X-Y-Z";
break;
}
if(dErr < dThisErr)dErr = dThisErr;
}
}
}
return dErr;
//double[] dLoc = forwardKinematics(dZ);
// String sIn = "";
// String sOut = "";
// String sDiff = "";
// for(int iIdx = 0; iIdx < 3; iIdx++){
// //sOut += Double.toString(dP[iIdx] - dLoc[iIdx]) + " ";
// sIn += String.format("IN =>%.2f, ", new Object[]{dP[iIdx]});
// sOut += String.format("OUT=>%.2f, ", new Object[]{dLoc[iIdx]});
// sDiff += String.format("DIF=>%.2f, ", new Object[]{dP[iIdx] - dLoc[iIdx]});
// }
//// System.out.println(sIn);
//// System.out.println(sOut);
// System.out.println(sDiff);
}
/*
* The forward kinematics calculate the intersection of three spheres centered on the points
* on each column with the given Z value. The equations for these spheres are:
* (dX - dX0)^2 + (dY - dY0)^2 + (dZ - dZ0)^2 = dArmSqrd
* (dX - dX1)^2 + (dY - dY1)^2 + (dZ - dZ1)^2 = dArmSqrd
* (dX - dX2)^2 + (dY - dY2)^2 + (dZ - dZ2)^2 = dArmSqrd
*
* We have three equations with three unknowns, but because of the squares, the solutions aren't trivial.
* So we need to find a frame of reference that simplifies the problem. Upon inspection we can see that we
* have a tetrahedron where we know 3 points (and therefore the lengths the sides between those points) and
* length of the sides (dArmLen) from each of those points to the fourth unknown point. Starting with the
* three points as the base of an irregular pyramid (tetrahedron) and using vectors we can find the fourth
* point. Because we have three equal sides we have three equilateral triangles. With the unknown point at
* the apex of these equilateral triangles. One property of equilateral triangles is that a line at a right
* angle to the midpoint of the base will intersect the apex. If we view the tetrahedron from a point at a
* right angle to the plane including the three bases, this property still applies. So the point where a
* line at right angles to this plane intersects the apex (unknown point) is at the intersection of the
* three lines going at right angles to the midpoints of the bases. In reality, the third line is redundant,
* one only needs the intersection of two lines to define a point. With the lengths of these lines one can
* also calculate the length of the right angle line from this point to the apex. Using vector math one can
* define the point in the original coordinate system.
*/
double[] forwardKinematics(double[] dZ){
double[][] dColP = new double[3][3];
for(int iIdx = 0; iIdx < 3; iIdx++){