Hit Detection on Scaled or Rotated Lines?
Hi,
I'm writing a java program in which I paint a number of line segments.
I've added the ability to scale or rotate the scene.Now, if the scene
is neither scaled or rotated, then detecting when the mouse pointer
is over a particular line is not especially difficult.I just iterate through
all the line objects and compare their coordinates with the mouse's
coordinates.
But I can't figure how to detect when the mouse is over a line when the
scene has been scaled and/or rotated.I guess this problem requires
a bit of maths - something I'm bit rusty at.Can someone help me?
Thanks.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.Hashtable;
import javax.swing.*;
import javax.swing.event.*;
public class TransformSelection extends JPanel implements ChangeListener {
JSlider rotateSlider;
JSlider scaleSlider;
Polygon polygon;
Line2D[] lines;
int selectedIndex = -1;
Color[] colors = {
Color.red, Color.green.darker(), Color.blue, Color.magenta, Color.orange
};
AffineTransform at = new AffineTransform();
double theta = 0;
double scale = 1.0;
public void stateChanged(ChangeEvent e) {
JSlider slider = (JSlider)e.getSource();
int value = slider.getValue();
double cx = getWidth()/2.0;
double cy = getHeight()/2.0;
if(slider == rotateSlider) {
theta = Math.toRadians(value);
}
if(slider == scaleSlider) {
scale = value/100.0;
}
at.setToTranslation((1.0-scale)*cx, (1.0-scale)*cy);
at.scale(scale, scale);
at.rotate(theta, cx, cy);
repaint();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
if(lines == null) {
initLines();
}
AffineTransform orig = g2.getTransform();
g2.setTransform(at);
//g2.setPaint(Color.blue);
//g2.draw(polygon);
g2.setPaint(Color.red);
for(int j = 0; j < lines.length; j++) {
g2.setPaint(colors[j]);
g2.draw(lines[j]);
if(j == selectedIndex) {
g2.setPaint(Color.red);
double cx = (lines[j].getX1() + lines[j].getX2())/2;
double cy = (lines[j].getY1() + lines[j].getY2())/2;
g2.draw(new Ellipse2D.Double(cx-4, cy-4, 8, 8));
}
}
g2.setTransform(orig);
}
public void setSelectedIndex(int index) {
selectedIndex = index;
repaint();
}
private void initLines() {
int w = getWidth();
int h = getHeight();
double cx = w/2.0;
double cy = h/2.0;
int R = Math.min(w,h)/6;
int sides = 5;
int[][]xy = generateShapeArrays(w/2, h/2, R, sides);
polygon = new Polygon(xy[0], xy[1], 5);
lines = new Line2D[sides];
double theta = -Math.PI/2;
for(int j = 0; j < sides; j++) {
double x1 = cx + (R/2)*Math.cos(theta);
double y1 = cy + (R/2)*Math.sin(theta);
double x2 = polygon.xpoints[j];
double y2 = polygon.ypoints[j];
lines[j] = new Line2D.Double(x1, y1, x2, y2);
theta += 2*Math.PI/sides;
}
}
private JPanel getControls() {
rotateSlider = new JSlider(JSlider.HORIZONTAL, -180, 180, 0);
rotateSlider.setMajorTickSpacing(30);
rotateSlider.setMinorTickSpacing(10);
rotateSlider.setPaintTicks(true);
rotateSlider.setPaintLabels(true);
rotateSlider.addChangeListener(this);
scaleSlider = new JSlider(JSlider.HORIZONTAL, 50, 200, 100);
scaleSlider.setMajorTickSpacing(50);
scaleSlider.setMinorTickSpacing(10);
scaleSlider.setPaintTicks(true);
scaleSlider.setLabelTable(getLabelTable(50, 200, 50));
scaleSlider.setPaintLabels(true);
scaleSlider.addChangeListener(this);
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1.0;
gbc.fill = gbc.HORIZONTAL;
gbc.gridwidth = gbc.REMAINDER;
panel.add(rotateSlider, gbc);
panel.add(scaleSlider, gbc);
return panel;
}
private Hashtable getLabelTable(int min, int max, int inc) {
Hashtable<Integer,JComponent> table = new Hashtable<Integer,JComponent>();
for(int j = min; j <= max; j += inc) {
String s = String.format("%d%%", j);
table.put(new Integer(j), new JLabel(s));
}
return table;
}
private int[][] generateShapeArrays(int cx, int cy, int R, int sides) {
int radInc = 0;
if(sides % 2 == 0) {
radInc = 1;
}
int[] x = new int[sides];
int[] y = new int[sides];
for(int i = 0; i < sides; i++) {
x[i] = cx + (int)(R * Math.sin(radInc*Math.PI/sides));
y[i] = cy - (int)(R * Math.cos(radInc*Math.PI/sides));
radInc += 2;
}
// keep base of triangle level
if(sides == 3) {
y[2] = y[1];
}
return new int[][] { x, y };
}
public static void main(String[] args) {
TransformSelection test = new TransformSelection();
test.addMouseListener(new LineSelector(test));
JFrame f = new JFrame("click on lines");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(test);
f.getContentPane().add(test.getControls(), "Last");
f.setSize(400,400);
f.setLocation(200,200);
f.setVisible(true);
}
}
class LineSelector extends MouseAdapter {
TransformSelection transformSelection;
Rectangle net;
int lastSelectedIndex;
final int SIDE = 4;
public LineSelector(TransformSelection ts) {
transformSelection = ts;
net = new Rectangle(SIDE, SIDE);
}
public void mousePressed(MouseEvent e) {
net.setLocation(e.getX() - SIDE/2, e.getY() - SIDE/2);
AffineTransform at = transformSelection.at;
Line2D[] lines = transformSelection.lines;
for(int j = 0; j < lines.length; j++) {
Shape xs = at.createTransformedShape(lines[j]);
if(xs.intersects(net)) {
transformSelection.setSelectedIndex(j);
lastSelectedIndex = j;
break;
}
}
}
}