summaryrefslogtreecommitdiffstats
path: root/src/net/java/joglutils/msg
diff options
context:
space:
mode:
authorKenneth Russel <[email protected]>2007-03-30 22:03:53 +0000
committerKenneth Russel <[email protected]>2007-03-30 22:03:53 +0000
commit970468d9ee5fa1e5e316413307fca7a6bdb951f0 (patch)
tree9e8b676a7596783c1bcd4769bb001e88dcd97c90 /src/net/java/joglutils/msg
parent4ed015dfa00737f1a19934f77372971bd807b7aa (diff)
First refactoring: separated DisplayShelf and DisplayShelfRenderer;
fixed context issues to allow GLJPanel to properly render display shelf (Slider temporarily removed; functionality will re-appear in later refactoring) git-svn-id: file:///usr/local/projects/SUN/JOGL/git-svn/svn-server-sync/joglutils/trunk@51 83d24430-9974-4f80-8418-2cc3294053b9
Diffstat (limited to 'src/net/java/joglutils/msg')
-rw-r--r--src/net/java/joglutils/msg/test/DisplayShelf.java578
-rw-r--r--src/net/java/joglutils/msg/test/DisplayShelfRenderer.java639
2 files changed, 646 insertions, 571 deletions
diff --git a/src/net/java/joglutils/msg/test/DisplayShelf.java b/src/net/java/joglutils/msg/test/DisplayShelf.java
index b78f73a..02d9e28 100644
--- a/src/net/java/joglutils/msg/test/DisplayShelf.java
+++ b/src/net/java/joglutils/msg/test/DisplayShelf.java
@@ -37,585 +37,20 @@
package net.java.joglutils.msg.test;
-import java.awt.BasicStroke;
import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Container;
import java.awt.DisplayMode;
import java.awt.Frame;
-import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.*;
-import java.awt.image.*;
-import java.net.*;
-import java.util.*;
-import javax.imageio.*;
+
import javax.swing.*;
-import javax.swing.event.*;
import javax.media.opengl.*;
-import com.sun.opengl.util.j2d.*;
-
-import net.java.joglutils.msg.actions.*;
-import net.java.joglutils.msg.collections.*;
-import net.java.joglutils.msg.math.*;
-import net.java.joglutils.msg.misc.*;
-import net.java.joglutils.msg.nodes.*;
/** A test implementing a 3D display shelf component. */
-public class DisplayShelf extends Container {
- private GLCanvas canvas;
-
- private float DEFAULT_ASPECT_RATIO = 0.665f;
- // This also affects the spacing
- private float DEFAULT_HEIGHT = 1.5f;
- private float DEFAULT_ON_SCREEN_FRAC = 0.5f;
- private float EDITING_ON_SCREEN_FRAC = 0.95f;
- private float offsetFrac;
-
- private float STACKED_SPACING_FRAC = 0.3f;
- private float SELECTED_SPACING_FRAC = 0.6f;
- private float EDITED_SPACING_FRAC = 1.5f;
-
- // This is how much we raise the geometry above the floor in single image mode
- private float SINGLE_IMAGE_MODE_RAISE_FRAC = 2.0f;
-
- // The camera
- private PerspectiveCamera camera;
-
- static class TitleGraph {
- String url;
- Separator sep = new Separator();
- Transform xform = new Transform();
- Texture2 texture = new Texture2();
- Coordinate3 coords = new Coordinate3();
-
- TitleGraph(String url) {
- this.url = url;
- }
- }
-
- private Group root;
- private Separator imageRoot;
- private String[] images;
- private List<TitleGraph> titles = new ArrayList<TitleGraph>();
- private JSlider slider;
- private int targetIndex;
- // This encodes both the current position and the horizontal animation alpha
- private float currentIndex;
- // This encodes the animation alpha for the z-coordinate motion
- // associated with going in to and out of editing mode
- private float currentZ;
- private float targetZ;
- // This is effectively a constant
- private float viewingZ;
- // This is also currently effectively a constant, though we need to
- // compute it dynamically for each picture to get it to show up
- // centered
- private float editingZ;
- // This encodes our current Y coordinate in editing mode
- private float currentY;
- // This encodes our target Y coordinate in editing mode
- private float targetY;
- // If the difference between the current and target values of any of
- // the above are > EPSILON, then we will continue repainting
- private static final float EPSILON = 1.0e-3f;
- private SystemTime time;
- private boolean animating;
- private boolean forceRecompute;
- // Single image mode toggle
- private boolean singleImageMode;
-
- // A scale factor for the animation speed
- private static final float ANIM_SCALE_FACTOR = 7.0f;
- // The rotation angle of the titles
- private static final float ROT_ANGLE = (float) Math.toRadians(75);
-
- // Visual progress of downloads
- private Texture2 clockTexture;
- private volatile boolean doneLoading;
-
- private void computeCoords(Coordinate3 coordNode, float aspectRatio) {
- Vec3fCollection coords = coordNode.getData();
- if (coords == null) {
- coords = new Vec3fCollection();
- Vec3f zero = new Vec3f();
- for (int i = 0; i < 6; i++) {
- coords.add(zero);
- }
- coordNode.setData(coords);
- }
- // Now compute the actual values
- Vec3f lowerLeft = new Vec3f(-0.5f * DEFAULT_HEIGHT * aspectRatio, 0, 0);
- Vec3f lowerRight = new Vec3f( 0.5f * DEFAULT_HEIGHT * aspectRatio, 0, 0);
- Vec3f upperLeft = new Vec3f(-0.5f * DEFAULT_HEIGHT * aspectRatio, DEFAULT_HEIGHT, 0);
- Vec3f upperRight = new Vec3f( 0.5f * DEFAULT_HEIGHT * aspectRatio, DEFAULT_HEIGHT, 0);
- // First triangle
- coords.set(0, upperRight);
- coords.set(1, upperLeft);
- coords.set(2, lowerLeft);
- // Second triangle
- coords.set(3, upperRight);
- coords.set(4, lowerLeft);
- coords.set(5, lowerRight);
- }
-
- private static void drawClock(Graphics2D g, int minsPastMidnight,
- int x, int y, int width, int height) {
- g.setColor(Color.DARK_GRAY);
- g.fillRect(x, y, width, height);
- g.setColor(Color.GRAY);
- int midx = (int) (x + (width / 2.0f));
- int midy = (int) (y + (height / 2.0f));
- int sz = (int) (0.8f * Math.min(width, height));
- g.setStroke(new BasicStroke(sz / 20.0f,
- BasicStroke.CAP_ROUND,
- BasicStroke.JOIN_MITER));
- int arcSz = (int) (0.4f * sz);
- int smallHandSz = (int) (0.3f * sz);
- int bigHandSz = (int) (0.4f * sz);
- g.drawRoundRect(midx - (sz / 2), midy - (sz / 2),
- sz, sz,
- arcSz, arcSz);
- float hour = minsPastMidnight / 60.0f;
- int min = minsPastMidnight % 60;
- float hourAngle = hour * 2.0f * (float) Math.PI / 12;
- float minAngle = min * 2.0f * (float) Math.PI / 60;
-
- g.drawLine(midx, midy,
- midx + (int) (smallHandSz * Math.cos(hourAngle)),
- midy + (int) (smallHandSz * Math.sin(hourAngle)));
- g.drawLine(midx, midy,
- midx + (int) (bigHandSz * Math.cos(minAngle)),
- midy + (int) (bigHandSz * Math.sin(minAngle)));
- }
-
- private void startLoading() {
- final List<TitleGraph> queuedGraphs = new ArrayList<TitleGraph>();
- queuedGraphs.addAll(titles);
-
- Thread loaderThread = new Thread(new Runnable() {
- public void run() {
- try {
- while (queuedGraphs.size() > 0) {
- TitleGraph graph = queuedGraphs.remove(0);
- BufferedImage img = null;
- try {
- img = ImageIO.read(new URL(graph.url));
- } catch (Exception e) {
- System.out.println("Exception loading " + graph.url + ":");
- e.printStackTrace();
- }
- if (img != null) {
- graph.sep.replaceChild(clockTexture, graph.texture);
- graph.texture.setTexture(img, false);
- // Figure out the new aspect ratio based on the image's width and height
- float aspectRatio = (float) img.getWidth() / (float) img.getHeight();
- // Compute new coordinates
- computeCoords(graph.coords, aspectRatio);
- // Schedule a repaint
- canvas.repaint();
- }
- }
- } finally {
- doneLoading = true;
- }
- }
- });
- // Avoid having the loader thread preempt the rendering thread
- loaderThread.setPriority(Thread.NORM_PRIORITY - 2);
- loaderThread.start();
- }
-
- private void startClockAnimation() {
- Thread clockAnimThread = new Thread(new Runnable() {
- public void run() {
- while (!doneLoading) {
- canvas.repaint();
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- }
- }
- }
- });
- clockAnimThread.start();
- }
-
- private void setTargetIndex(int index) {
- this.targetIndex = index;
- if (!animating) {
- time.rebase();
- }
- recomputeTargetYZ(true);
- canvas.repaint();
- }
-
- private void recomputeTargetYZ(boolean animate) {
- if (singleImageMode) {
- // Compute a target Y and Z depth based on the image we want to view
-
- // FIXME: right now the Y and Z targets are always the same, but
- // once we adjust the images to fit within a bounding square,
- // they won't be
- targetY = (0.5f + SINGLE_IMAGE_MODE_RAISE_FRAC) * DEFAULT_HEIGHT;
- editingZ = 0.5f * DEFAULT_HEIGHT / (EDITING_ON_SCREEN_FRAC * (float) Math.tan(camera.getHeightAngle()));
- targetZ = editingZ;
- } else {
- targetY = 0.5f * DEFAULT_HEIGHT;
- targetZ = viewingZ;
- }
-
- if (!animate) {
- currentY = targetY;
- currentZ = targetZ;
- currentIndex = targetIndex;
- }
- }
-
- private boolean recompute() {
- if (!forceRecompute) {
- if (Math.abs(targetIndex - currentIndex) < EPSILON &&
- Math.abs(targetZ - currentZ) < EPSILON &&
- Math.abs(targetY - currentY) < EPSILON)
- return false;
- }
-
- forceRecompute = false;
-
- time.update();
- float deltaT = (float) time.deltaT();
-
- // Make the animation speed independent of frame rate
- currentIndex = currentIndex + (targetIndex - currentIndex) * deltaT * ANIM_SCALE_FACTOR;
- currentZ = currentZ + (targetZ - currentZ) * deltaT * ANIM_SCALE_FACTOR;
- currentY = currentY + (targetY - currentY) * deltaT * ANIM_SCALE_FACTOR;
- // An alpha of 0 indicates we're fully in viewing mode
- // An alpha of 1 indicates we're fully in editing mode
- float zAlpha = (currentZ - viewingZ) / (editingZ - viewingZ);
-
- // Recompute the positions and orientations of each title, and the position of the camera
- int firstIndex = (int) Math.floor(currentIndex);
- int secondIndex = (int) Math.ceil(currentIndex);
- if (secondIndex == firstIndex) {
- secondIndex = firstIndex + 1;
- }
-
- float alpha = currentIndex - firstIndex;
-
- int idx = 0;
- float curPos = 0.0f;
- float stackedSpacing = DEFAULT_HEIGHT * (zAlpha * EDITED_SPACING_FRAC + (1.0f - zAlpha) * STACKED_SPACING_FRAC);
- float selectedSpacing = DEFAULT_HEIGHT * (zAlpha * EDITED_SPACING_FRAC + (1.0f - zAlpha) * SELECTED_SPACING_FRAC);
- float angle = (1.0f - zAlpha) * ROT_ANGLE;
- float y = zAlpha * DEFAULT_HEIGHT * SINGLE_IMAGE_MODE_RAISE_FRAC;
- Rotf posAngle = new Rotf(Vec3f.Y_AXIS, angle);
- Rotf negAngle = new Rotf(Vec3f.Y_AXIS, -angle);
- float offset = 0;
-
- // Only bump the selected title out of the list if we're in viewing mode and close to it
- if (Math.abs(targetIndex - currentIndex) < 3.0) {
- offset = (1.0f - zAlpha) * offsetFrac * DEFAULT_HEIGHT;
- }
- for (TitleGraph graph : titles) {
- if (idx < firstIndex) {
- graph.xform.getTransform().setRotation(posAngle);
- graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, 0));
- curPos += stackedSpacing;
- } else if (idx > secondIndex) {
- graph.xform.getTransform().setRotation(negAngle);
- graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, 0));
- curPos += stackedSpacing;
- } else if (idx == firstIndex) {
- // Bump the position of this title
- curPos += (1.0f - alpha) * (selectedSpacing - stackedSpacing);
-
- // The camera is glued to this position
- float cameraPos = curPos + alpha * selectedSpacing;
-
- // Interpolate
- graph.xform.getTransform().setRotation(new Rotf(Vec3f.Y_AXIS, alpha * angle));
- graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, (1.0f - alpha) * offset));
-
- // Now recompute the position of the camera
- // Aim to get the titles to fill a certain fraction of the vertical field of view
- camera.setPosition(new Vec3f(cameraPos,
- currentY,
- currentZ));
-
- // Maintain this much distance between the two animating titles
- curPos += selectedSpacing;
- } else {
- // Interpolate
- graph.xform.getTransform().setRotation(new Rotf(Vec3f.Y_AXIS, (1.0f - alpha) * -angle));
- graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, alpha * offset));
-
- curPos += stackedSpacing + alpha * (selectedSpacing - stackedSpacing);
- }
-
- ++idx;
- }
-
- return true;
- }
-
- public DisplayShelf(Group root, String[] images) {
- this.images = images;
- this.root = root;
- time = new SystemTime();
- time.rebase();
- setLayout(new BorderLayout());
- camera = new PerspectiveCamera();
- camera.setNearDistance(1.0f);
- camera.setFarDistance(100.0f);
- camera.setHeightAngle((float) Math.PI / 8);
- // This could / should be computed elsewhere, especially if we add
- // the ability to dynamically adjust the camera's height angle
- viewingZ = 0.5f * DEFAULT_HEIGHT / (DEFAULT_ON_SCREEN_FRAC * (float) Math.tan(camera.getHeightAngle()));
- // Compute the fraction by which we offset the selected title
- // based on a couple of known good points
- offsetFrac = (float) (((3 * Math.PI / 40) / camera.getHeightAngle()) + 0.1f);
- canvas = new GLCanvas();
- canvas.addGLEventListener(new Listener());
- canvas.addMouseListener(new MListener());
- canvas.addKeyListener(new KeyAdapter() {
- public void keyPressed(KeyEvent e) {
- switch (e.getKeyCode()) {
- case KeyEvent.VK_SPACE:
- setSingleImageMode(!getSingleImageMode(), true);
- break;
-
- case KeyEvent.VK_ENTER:
- setSingleImageMode(!getSingleImageMode(), false);
- break;
-
- case KeyEvent.VK_LEFT:
- slider.setValue(Math.max(0, targetIndex - 1));
- break;
-
- case KeyEvent.VK_RIGHT:
- slider.setValue(Math.min(titles.size() - 1, targetIndex + 1));
- break;
- }
- }
- });
- add(canvas, BorderLayout.CENTER);
- slider = new JSlider(0, images.length - 1, 0);
- slider.addChangeListener(new ChangeListener() {
- public void stateChanged(ChangeEvent e) {
- setTargetIndex(slider.getValue());
- }
- });
- add(slider, BorderLayout.SOUTH);
- }
-
- public void setSingleImageMode(boolean singleImageMode, boolean animateTransition) {
- this.singleImageMode = singleImageMode;
- if (!animating) {
- time.rebase();
- }
- recomputeTargetYZ(animateTransition);
- forceRecompute = !animateTransition;
- canvas.repaint();
- }
-
- public boolean getSingleImageMode() {
- return singleImageMode;
- }
-
- class Listener implements GLEventListener {
- private GLRenderAction ra = new GLRenderAction();
-
- public void init(GLAutoDrawable drawable) {
- GL gl = drawable.getGL();
-
- // Build the scene graph
- root.removeAllChildren();
-
- // The clock
- clockTexture = new Texture2();
- clockTexture.initTextureRenderer((int) (300 * DEFAULT_HEIGHT * DEFAULT_ASPECT_RATIO),
- (int) (300 * DEFAULT_HEIGHT),
- false);
-
- // The images
- imageRoot = new Separator();
-
- // The mirrored images, under the floor
- Separator mirrorRoot = new Separator();
-
- Transform mirrorXform = new Transform();
- // Mirror vertically
- mirrorXform.getTransform().set(1, 1, -1.0f);
- mirrorRoot.addChild(mirrorXform);
- // Assume we know what we're doing here with setting per-vertex
- // colors for each piece of geometry in one shot
- Color4 colorNode = new Color4();
- Vec4fCollection colors = new Vec4fCollection();
- Vec4f fadeTop = new Vec4f(0.75f, 0.75f, 0.75f, 0.75f);
- Vec4f fadeBot = new Vec4f(0.25f, 0.25f, 0.25f, 0.25f);
- // First triangle
- colors.add(fadeTop);
- colors.add(fadeTop);
- colors.add(fadeBot);
- // Second triangle
- colors.add(fadeTop);
- colors.add(fadeBot);
- colors.add(fadeBot);
- colorNode.setData(colors);
- mirrorRoot.addChild(colorNode);
-
- TriangleSet tris = new TriangleSet();
- tris.setNumTriangles(2);
-
- int i = 0;
- for (String image : images) {
- TitleGraph graph = new TitleGraph(image);
- titles.add(graph);
- computeCoords(graph.coords, DEFAULT_ASPECT_RATIO);
- graph.xform.getTransform().setTranslation(new Vec3f(i, 0, 0));
- Separator sep = graph.sep;
- sep.addChild(graph.xform);
- sep.addChild(graph.coords);
- // Add in the clock texture at the beginning
- sep.addChild(clockTexture);
- TextureCoordinate2 texCoordNode = new TextureCoordinate2();
- Vec2fCollection texCoords = new Vec2fCollection();
- // Texture coordinates for two triangles
- // First triangle
- texCoords.add(new Vec2f( 1, 1));
- texCoords.add(new Vec2f( 0, 1));
- texCoords.add(new Vec2f( 0, 0));
- // Second triangle
- texCoords.add(new Vec2f( 1, 1));
- texCoords.add(new Vec2f( 0, 0));
- texCoords.add(new Vec2f( 1, 0));
- texCoordNode.setData(texCoords);
- sep.addChild(texCoordNode);
-
- sep.addChild(tris);
-
- // Add this to each rendering root
- imageRoot.addChild(sep);
- mirrorRoot.addChild(sep);
-
- ++i;
- }
-
- // Now produce the floor geometry
- float maxSpacing = DEFAULT_HEIGHT * Math.max(STACKED_SPACING_FRAC, Math.max(SELECTED_SPACING_FRAC, EDITED_SPACING_FRAC));
- float minx = -i * maxSpacing;
- float maxx = 2 * i * maxSpacing;
- // Furthest back from the camera
- float minz = -2 * DEFAULT_HEIGHT;
- // Assume this will be close enough to cover all of the mirrored geometry
- float maxz = DEFAULT_HEIGHT;
- Separator floorRoot = new Separator();
- Blend blend = new Blend();
- blend.setEnabled(true);
- blend.setSourceFunc(Blend.ONE);
- blend.setDestFunc(Blend.ONE_MINUS_SRC_ALPHA);
- floorRoot.addChild(blend);
- Coordinate3 floorCoords = new Coordinate3();
- floorCoords.setData(new Vec3fCollection());
- // First triangle
- floorCoords.getData().add(new Vec3f(maxx, 0, minz));
- floorCoords.getData().add(new Vec3f(minx, 0, minz));
- floorCoords.getData().add(new Vec3f(minx, 0, maxz));
- // Second triangle
- floorCoords.getData().add(new Vec3f(maxx, 0, minz));
- floorCoords.getData().add(new Vec3f(minx, 0, maxz));
- floorCoords.getData().add(new Vec3f(maxx, 0, maxz));
- floorRoot.addChild(floorCoords);
- // Colors
- Vec4f gray = new Vec4f(0.4f, 0.4f, 0.4f, 0.4f);
- Vec4f clearGray = new Vec4f(0.0f, 0.0f, 0.0f, 0.0f);
- Color4 floorColors = new Color4();
- floorColors.setData(new Vec4fCollection());
- // First triangle
- floorColors.getData().add(gray);
- floorColors.getData().add(gray);
- floorColors.getData().add(clearGray);
- // Second triangle
- floorColors.getData().add(gray);
- floorColors.getData().add(clearGray);
- floorColors.getData().add(clearGray);
- floorRoot.addChild(floorColors);
-
- floorRoot.addChild(tris);
-
- // Now set up the overall scene graph
- root.addChild(camera);
- root.addChild(imageRoot);
- root.addChild(mirrorRoot);
- root.addChild(floorRoot);
-
- startLoading();
- startClockAnimation();
- recomputeTargetYZ(false);
- forceRecompute = true;
- recompute();
- }
-
- public void display(GLAutoDrawable drawable) {
- // Recompute position of camera and orientation of images
- boolean repaintAgain = recompute();
-
- if (!doneLoading) {
- if (!repaintAgain) {
- time.update();
- }
-
- TextureRenderer rend = clockTexture.getTextureRenderer();
- Graphics2D g = rend.createGraphics();
- drawClock(g, (int) (time.time() * 30),
- 0, 0, rend.getWidth(), rend.getHeight());
- g.dispose();
- rend.markDirty(0, 0, rend.getWidth(), rend.getHeight());
- }
-
- // Redraw
- GL gl = drawable.getGL();
- gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
- ra.apply(root);
-
- if (repaintAgain) {
- animating = true;
- canvas.repaint();
- } else {
- animating = false;
- }
- }
-
- public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
- }
-
- public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {}
- }
-
- class MListener extends MouseAdapter {
- RayPickAction ra = new RayPickAction();
-
- public void mousePressed(MouseEvent e) {
- ra.setPoint(e.getX(), e.getY(), e.getComponent());
- // Apply to the scene root
- ra.apply(root);
- List<PickedPoint> pickedPoints = ra.getPickedPoints();
- Path p = null;
- if (!pickedPoints.isEmpty())
- p = pickedPoints.get(0).getPath();
- if (p != null && p.size() > 1) {
- int idx = imageRoot.findChild(p.get(p.size() - 2));
- if (idx >= 0) {
- // Need to keep the slider and this mechanism in sync
- slider.setValue(idx);
- }
- }
- }
- }
-
+public class DisplayShelf {
public static void main(String[] args) {
Frame f = new Frame("Display Shelf test");
f.setLayout(new BorderLayout());
@@ -685,10 +120,11 @@ public class DisplayShelf extends Container {
"http://download.java.net/media/jogl/builds/ds_tmp/mzi.uswlslxx.200x200-75.jpg"
};
- Separator root = new Separator();
-
- DisplayShelf shelf = new DisplayShelf(root, images);
- f.add(shelf);
+ DisplayShelfRenderer renderer = new DisplayShelfRenderer(images);
+ GLCanvas canvas = new GLCanvas(new GLCapabilities(), null, renderer.getSharedContext(), null);
+ canvas.setFocusable(true);
+ canvas.addGLEventListener(renderer);
+ f.add(canvas);
GraphicsDevice dev = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
DisplayMode curMode = dev.getDisplayMode();
int height = (int) (0.5f * curMode.getWidth());
diff --git a/src/net/java/joglutils/msg/test/DisplayShelfRenderer.java b/src/net/java/joglutils/msg/test/DisplayShelfRenderer.java
new file mode 100644
index 0000000..6d87a87
--- /dev/null
+++ b/src/net/java/joglutils/msg/test/DisplayShelfRenderer.java
@@ -0,0 +1,639 @@
+/*
+ * Copyright (c) 2007 Sun Microsystems, Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistribution of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistribution in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ *
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ *
+ */
+
+package net.java.joglutils.msg.test;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.event.*;
+import java.awt.image.*;
+import java.net.*;
+import java.util.*;
+import javax.imageio.*;
+
+import javax.media.opengl.*;
+import com.sun.opengl.util.j2d.*;
+
+import net.java.joglutils.msg.actions.*;
+import net.java.joglutils.msg.collections.*;
+import net.java.joglutils.msg.math.*;
+import net.java.joglutils.msg.misc.*;
+import net.java.joglutils.msg.nodes.*;
+
+public class DisplayShelfRenderer implements GLEventListener {
+ private float DEFAULT_ASPECT_RATIO = 0.665f;
+ // This also affects the spacing
+ private float DEFAULT_HEIGHT = 1.5f;
+ private float DEFAULT_ON_SCREEN_FRAC = 0.5f;
+ private float EDITING_ON_SCREEN_FRAC = 0.95f;
+ private float offsetFrac;
+
+ private float STACKED_SPACING_FRAC = 0.3f;
+ private float SELECTED_SPACING_FRAC = 0.6f;
+ private float EDITED_SPACING_FRAC = 1.5f;
+
+ // This is how much we raise the geometry above the floor in single image mode
+ private float SINGLE_IMAGE_MODE_RAISE_FRAC = 2.0f;
+
+ // The camera
+ private PerspectiveCamera camera;
+
+ static class TitleGraph {
+ String url;
+ Separator sep = new Separator();
+ Transform xform = new Transform();
+ Texture2 texture = new Texture2();
+ Coordinate3 coords = new Coordinate3();
+
+ TitleGraph(String url) {
+ this.url = url;
+ }
+ }
+
+ // This is used to avoid having to re-initialize textures during
+ // resizes of Swing components
+ private GLPbuffer sharedPbuffer;
+ private boolean firstInit = true;
+
+ private GLAutoDrawable drawable;
+
+ private Separator root;
+ private Separator imageRoot;
+ private String[] images;
+ private List<TitleGraph> titles = new ArrayList<TitleGraph>();
+ private GLRenderAction ra = new GLRenderAction();
+ private int targetIndex;
+ // This encodes both the current position and the horizontal animation alpha
+ private float currentIndex;
+ // This encodes the animation alpha for the z-coordinate motion
+ // associated with going in to and out of editing mode
+ private float currentZ;
+ private float targetZ;
+ // This is effectively a constant
+ private float viewingZ;
+ // This is also currently effectively a constant, though we need to
+ // compute it dynamically for each picture to get it to show up
+ // centered
+ private float editingZ;
+ // This encodes our current Y coordinate in editing mode
+ private float currentY;
+ // This encodes our target Y coordinate in editing mode
+ private float targetY;
+ // If the difference between the current and target values of any of
+ // the above are > EPSILON, then we will continue repainting
+ private static final float EPSILON = 1.0e-3f;
+ private SystemTime time;
+ private boolean animating;
+ private boolean forceRecompute;
+ // Single image mode toggle
+ private boolean singleImageMode;
+
+ // A scale factor for the animation speed
+ private static final float ANIM_SCALE_FACTOR = 7.0f;
+ // The rotation angle of the titles
+ private static final float ROT_ANGLE = (float) Math.toRadians(75);
+
+ // Visual progress of downloads
+ private Texture2 clockTexture;
+ private volatile boolean doneLoading;
+
+ public DisplayShelfRenderer(String[] images) {
+ // Create a small pbuffer with which we share textures and display
+ // lists to avoid having to reload textures during repeated calls
+ // to init()
+ sharedPbuffer = GLDrawableFactory.getFactory().createGLPbuffer(new GLCapabilities(), null, 1, 1, null);
+ sharedPbuffer.display();
+
+ this.images = images;
+ root = new Separator();
+ time = new SystemTime();
+ time.rebase();
+ camera = new PerspectiveCamera();
+ camera.setNearDistance(1.0f);
+ camera.setFarDistance(100.0f);
+ camera.setHeightAngle((float) Math.PI / 8);
+ // This could / should be computed elsewhere, especially if we add
+ // the ability to dynamically adjust the camera's height angle
+ viewingZ = 0.5f * DEFAULT_HEIGHT / (DEFAULT_ON_SCREEN_FRAC * (float) Math.tan(camera.getHeightAngle()));
+ // Compute the fraction by which we offset the selected title
+ // based on a couple of known good points
+ offsetFrac = (float) (((3 * Math.PI / 40) / camera.getHeightAngle()) + 0.1f);
+ }
+
+ /** Callers must share textures and display lists with this context
+ for correct behavior of this renderer. It is used to avoid
+ repeated reloading of textures when resizing the renderer
+ embedded in a GLJPanel. */
+ public GLContext getSharedContext() {
+ return sharedPbuffer.getContext();
+ }
+
+ public void setSingleImageMode(boolean singleImageMode, boolean animateTransition) {
+ this.singleImageMode = singleImageMode;
+ if (!animating) {
+ time.rebase();
+ }
+ recomputeTargetYZ(animateTransition);
+ forceRecompute = !animateTransition;
+ if (drawable != null) {
+ drawable.repaint();
+ }
+ }
+
+ public boolean getSingleImageMode() {
+ return singleImageMode;
+ }
+
+ public int getNumImages() {
+ return titles.size();
+ }
+
+ public void setTargetIndex(int index) {
+ if (targetIndex == index)
+ return;
+
+ this.targetIndex = index;
+ if (!animating) {
+ time.rebase();
+ }
+ recomputeTargetYZ(true);
+ if (drawable != null) {
+ drawable.repaint();
+ }
+ }
+
+ public int getTargetIndex() {
+ return targetIndex;
+ }
+
+ public void init(GLAutoDrawable drawable) {
+ this.drawable = drawable;
+ GL gl = drawable.getGL();
+
+ if (firstInit) {
+ firstInit = false;
+
+ // Build the scene graph
+
+ // The clock
+ clockTexture = new Texture2();
+ clockTexture.initTextureRenderer((int) (300 * DEFAULT_HEIGHT * DEFAULT_ASPECT_RATIO),
+ (int) (300 * DEFAULT_HEIGHT),
+ false);
+
+ // The images
+ imageRoot = new Separator();
+
+ // The mirrored images under the floor
+ Separator mirrorRoot = new Separator();
+
+ Transform mirrorXform = new Transform();
+ // Mirror vertically
+ mirrorXform.getTransform().set(1, 1, -1.0f);
+ mirrorRoot.addChild(mirrorXform);
+ // Assume we know what we're doing here with setting per-vertex
+ // colors for each piece of geometry in one shot
+ Color4 colorNode = new Color4();
+ Vec4fCollection colors = new Vec4fCollection();
+ Vec4f fadeTop = new Vec4f(0.75f, 0.75f, 0.75f, 0.75f);
+ Vec4f fadeBot = new Vec4f(0.25f, 0.25f, 0.25f, 0.25f);
+ // First triangle
+ colors.add(fadeTop);
+ colors.add(fadeTop);
+ colors.add(fadeBot);
+ // Second triangle
+ colors.add(fadeTop);
+ colors.add(fadeBot);
+ colors.add(fadeBot);
+ colorNode.setData(colors);
+ mirrorRoot.addChild(colorNode);
+
+ TriangleSet tris = new TriangleSet();
+ tris.setNumTriangles(2);
+
+ int i = 0;
+ for (String image : images) {
+ TitleGraph graph = new TitleGraph(image);
+ titles.add(graph);
+ computeCoords(graph.coords, DEFAULT_ASPECT_RATIO);
+ graph.xform.getTransform().setTranslation(new Vec3f(i, 0, 0));
+ Separator sep = graph.sep;
+ sep.addChild(graph.xform);
+ sep.addChild(graph.coords);
+ // Add in the clock texture at the beginning
+ sep.addChild(clockTexture);
+ TextureCoordinate2 texCoordNode = new TextureCoordinate2();
+ Vec2fCollection texCoords = new Vec2fCollection();
+ // Texture coordinates for two triangles
+ // First triangle
+ texCoords.add(new Vec2f( 1, 1));
+ texCoords.add(new Vec2f( 0, 1));
+ texCoords.add(new Vec2f( 0, 0));
+ // Second triangle
+ texCoords.add(new Vec2f( 1, 1));
+ texCoords.add(new Vec2f( 0, 0));
+ texCoords.add(new Vec2f( 1, 0));
+ texCoordNode.setData(texCoords);
+ sep.addChild(texCoordNode);
+
+ sep.addChild(tris);
+
+ // Add this to each rendering root
+ imageRoot.addChild(sep);
+ mirrorRoot.addChild(sep);
+
+ ++i;
+ }
+
+ // Now produce the floor geometry
+ float maxSpacing = DEFAULT_HEIGHT * Math.max(STACKED_SPACING_FRAC, Math.max(SELECTED_SPACING_FRAC, EDITED_SPACING_FRAC));
+ float minx = -i * maxSpacing;
+ float maxx = 2 * i * maxSpacing;
+ // Furthest back from the camera
+ float minz = -2 * DEFAULT_HEIGHT;
+ // Assume this will be close enough to cover all of the mirrored geometry
+ float maxz = DEFAULT_HEIGHT;
+ Separator floorRoot = new Separator();
+ Blend blend = new Blend();
+ blend.setEnabled(true);
+ blend.setSourceFunc(Blend.ONE);
+ blend.setDestFunc(Blend.ONE_MINUS_SRC_ALPHA);
+ floorRoot.addChild(blend);
+ Coordinate3 floorCoords = new Coordinate3();
+ floorCoords.setData(new Vec3fCollection());
+ // First triangle
+ floorCoords.getData().add(new Vec3f(maxx, 0, minz));
+ floorCoords.getData().add(new Vec3f(minx, 0, minz));
+ floorCoords.getData().add(new Vec3f(minx, 0, maxz));
+ // Second triangle
+ floorCoords.getData().add(new Vec3f(maxx, 0, minz));
+ floorCoords.getData().add(new Vec3f(minx, 0, maxz));
+ floorCoords.getData().add(new Vec3f(maxx, 0, maxz));
+ floorRoot.addChild(floorCoords);
+ // Colors
+ Vec4f gray = new Vec4f(0.4f, 0.4f, 0.4f, 0.4f);
+ Vec4f clearGray = new Vec4f(0.0f, 0.0f, 0.0f, 0.0f);
+ Color4 floorColors = new Color4();
+ floorColors.setData(new Vec4fCollection());
+ // First triangle
+ floorColors.getData().add(gray);
+ floorColors.getData().add(gray);
+ floorColors.getData().add(clearGray);
+ // Second triangle
+ floorColors.getData().add(gray);
+ floorColors.getData().add(clearGray);
+ floorColors.getData().add(clearGray);
+ floorRoot.addChild(floorColors);
+
+ floorRoot.addChild(tris);
+
+ // Now set up the overall scene graph
+ root.addChild(camera);
+ root.addChild(imageRoot);
+ root.addChild(mirrorRoot);
+ root.addChild(floorRoot);
+
+ // Attach listeners (this is only for testing for now)
+ drawable.addMouseListener(new MListener());
+ drawable.addKeyListener(new KeyAdapter() {
+ public void keyPressed(KeyEvent e) {
+ switch (e.getKeyCode()) {
+ case KeyEvent.VK_SPACE:
+ setSingleImageMode(!getSingleImageMode(), true);
+ break;
+
+ case KeyEvent.VK_ENTER:
+ setSingleImageMode(!getSingleImageMode(), false);
+ break;
+
+ case KeyEvent.VK_LEFT:
+ setTargetIndex(Math.max(0, targetIndex - 1));
+ break;
+
+ case KeyEvent.VK_RIGHT:
+ setTargetIndex(Math.min(titles.size() - 1, targetIndex + 1));
+ break;
+ }
+ }
+ });
+
+ startLoading();
+ startClockAnimation();
+ recomputeTargetYZ(false);
+ forceRecompute = true;
+ recompute();
+ }
+ }
+
+ public void display(GLAutoDrawable drawable) {
+ // Recompute position of camera and orientation of images
+ boolean repaintAgain = recompute();
+
+ if (!doneLoading) {
+ if (!repaintAgain) {
+ time.update();
+ }
+
+ TextureRenderer rend = clockTexture.getTextureRenderer();
+ Graphics2D g = rend.createGraphics();
+ drawClock(g, (int) (time.time() * 30),
+ 0, 0, rend.getWidth(), rend.getHeight());
+ g.dispose();
+ rend.markDirty(0, 0, rend.getWidth(), rend.getHeight());
+ }
+
+ // Redraw
+ GL gl = drawable.getGL();
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
+ ra.apply(root);
+
+ if (repaintAgain) {
+ animating = true;
+ drawable.repaint();
+ } else {
+ animating = false;
+ }
+ }
+
+ public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
+ }
+
+ public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {}
+
+ //----------------------------------------------------------------------
+ // Internals only below this point
+ //
+
+ private void computeCoords(Coordinate3 coordNode, float aspectRatio) {
+ Vec3fCollection coords = coordNode.getData();
+ if (coords == null) {
+ coords = new Vec3fCollection();
+ Vec3f zero = new Vec3f();
+ for (int i = 0; i < 6; i++) {
+ coords.add(zero);
+ }
+ coordNode.setData(coords);
+ }
+ // Now compute the actual values
+ Vec3f lowerLeft = new Vec3f(-0.5f * DEFAULT_HEIGHT * aspectRatio, 0, 0);
+ Vec3f lowerRight = new Vec3f( 0.5f * DEFAULT_HEIGHT * aspectRatio, 0, 0);
+ Vec3f upperLeft = new Vec3f(-0.5f * DEFAULT_HEIGHT * aspectRatio, DEFAULT_HEIGHT, 0);
+ Vec3f upperRight = new Vec3f( 0.5f * DEFAULT_HEIGHT * aspectRatio, DEFAULT_HEIGHT, 0);
+ // First triangle
+ coords.set(0, upperRight);
+ coords.set(1, upperLeft);
+ coords.set(2, lowerLeft);
+ // Second triangle
+ coords.set(3, upperRight);
+ coords.set(4, lowerLeft);
+ coords.set(5, lowerRight);
+ }
+
+ private static void drawClock(Graphics2D g, int minsPastMidnight,
+ int x, int y, int width, int height) {
+ g.setColor(Color.DARK_GRAY);
+ g.fillRect(x, y, width, height);
+ g.setColor(Color.GRAY);
+ int midx = (int) (x + (width / 2.0f));
+ int midy = (int) (y + (height / 2.0f));
+ int sz = (int) (0.8f * Math.min(width, height));
+ g.setStroke(new BasicStroke(sz / 20.0f,
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_MITER));
+ int arcSz = (int) (0.4f * sz);
+ int smallHandSz = (int) (0.3f * sz);
+ int bigHandSz = (int) (0.4f * sz);
+ g.drawRoundRect(midx - (sz / 2), midy - (sz / 2),
+ sz, sz,
+ arcSz, arcSz);
+ float hour = minsPastMidnight / 60.0f;
+ int min = minsPastMidnight % 60;
+ float hourAngle = hour * 2.0f * (float) Math.PI / 12;
+ float minAngle = min * 2.0f * (float) Math.PI / 60;
+
+ g.drawLine(midx, midy,
+ midx + (int) (smallHandSz * Math.cos(hourAngle)),
+ midy + (int) (smallHandSz * Math.sin(hourAngle)));
+ g.drawLine(midx, midy,
+ midx + (int) (bigHandSz * Math.cos(minAngle)),
+ midy + (int) (bigHandSz * Math.sin(minAngle)));
+ }
+
+ private void startLoading() {
+ final List<TitleGraph> queuedGraphs = new ArrayList<TitleGraph>();
+ queuedGraphs.addAll(titles);
+
+ Thread loaderThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ while (queuedGraphs.size() > 0) {
+ TitleGraph graph = queuedGraphs.remove(0);
+ BufferedImage img = null;
+ try {
+ img = ImageIO.read(new URL(graph.url));
+ } catch (Exception e) {
+ System.out.println("Exception loading " + graph.url + ":");
+ e.printStackTrace();
+ }
+ if (img != null) {
+ graph.sep.replaceChild(clockTexture, graph.texture);
+ graph.texture.setTexture(img, false);
+ // Figure out the new aspect ratio based on the image's width and height
+ float aspectRatio = (float) img.getWidth() / (float) img.getHeight();
+ // Compute new coordinates
+ computeCoords(graph.coords, aspectRatio);
+ // Schedule a repaint
+ drawable.repaint();
+ }
+ }
+ } finally {
+ doneLoading = true;
+ }
+ }
+ });
+ // Avoid having the loader thread preempt the rendering thread
+ loaderThread.setPriority(Thread.NORM_PRIORITY - 2);
+ loaderThread.start();
+ }
+
+ private void startClockAnimation() {
+ Thread clockAnimThread = new Thread(new Runnable() {
+ public void run() {
+ while (!doneLoading) {
+ drawable.repaint();
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ });
+ clockAnimThread.start();
+ }
+
+ private void recomputeTargetYZ(boolean animate) {
+ if (singleImageMode) {
+ // Compute a target Y and Z depth based on the image we want to view
+
+ // FIXME: right now the Y and Z targets are always the same, but
+ // once we adjust the images to fit within a bounding square,
+ // they won't be
+ targetY = (0.5f + SINGLE_IMAGE_MODE_RAISE_FRAC) * DEFAULT_HEIGHT;
+ editingZ = 0.5f * DEFAULT_HEIGHT / (EDITING_ON_SCREEN_FRAC * (float) Math.tan(camera.getHeightAngle()));
+ targetZ = editingZ;
+ } else {
+ targetY = 0.5f * DEFAULT_HEIGHT;
+ targetZ = viewingZ;
+ }
+
+ if (!animate) {
+ currentY = targetY;
+ currentZ = targetZ;
+ currentIndex = targetIndex;
+ }
+ }
+
+ private boolean recompute() {
+ if (!forceRecompute) {
+ if (Math.abs(targetIndex - currentIndex) < EPSILON &&
+ Math.abs(targetZ - currentZ) < EPSILON &&
+ Math.abs(targetY - currentY) < EPSILON)
+ return false;
+ }
+
+ forceRecompute = false;
+
+ time.update();
+ float deltaT = (float) time.deltaT();
+
+ // Make the animation speed independent of frame rate
+ currentIndex = currentIndex + (targetIndex - currentIndex) * deltaT * ANIM_SCALE_FACTOR;
+ currentZ = currentZ + (targetZ - currentZ) * deltaT * ANIM_SCALE_FACTOR;
+ currentY = currentY + (targetY - currentY) * deltaT * ANIM_SCALE_FACTOR;
+ // An alpha of 0 indicates we're fully in viewing mode
+ // An alpha of 1 indicates we're fully in editing mode
+ float zAlpha = (currentZ - viewingZ) / (editingZ - viewingZ);
+
+ // Recompute the positions and orientations of each title, and the position of the camera
+ int firstIndex = (int) Math.floor(currentIndex);
+ int secondIndex = (int) Math.ceil(currentIndex);
+ if (secondIndex == firstIndex) {
+ secondIndex = firstIndex + 1;
+ }
+
+ float alpha = currentIndex - firstIndex;
+
+ int idx = 0;
+ float curPos = 0.0f;
+ float stackedSpacing = DEFAULT_HEIGHT * (zAlpha * EDITED_SPACING_FRAC + (1.0f - zAlpha) * STACKED_SPACING_FRAC);
+ float selectedSpacing = DEFAULT_HEIGHT * (zAlpha * EDITED_SPACING_FRAC + (1.0f - zAlpha) * SELECTED_SPACING_FRAC);
+ float angle = (1.0f - zAlpha) * ROT_ANGLE;
+ float y = zAlpha * DEFAULT_HEIGHT * SINGLE_IMAGE_MODE_RAISE_FRAC;
+ Rotf posAngle = new Rotf(Vec3f.Y_AXIS, angle);
+ Rotf negAngle = new Rotf(Vec3f.Y_AXIS, -angle);
+ float offset = 0;
+
+ // Only bump the selected title out of the list if we're in viewing mode and close to it
+ if (Math.abs(targetIndex - currentIndex) < 3.0) {
+ offset = (1.0f - zAlpha) * offsetFrac * DEFAULT_HEIGHT;
+ }
+ for (TitleGraph graph : titles) {
+ if (idx < firstIndex) {
+ graph.xform.getTransform().setRotation(posAngle);
+ graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, 0));
+ curPos += stackedSpacing;
+ } else if (idx > secondIndex) {
+ graph.xform.getTransform().setRotation(negAngle);
+ graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, 0));
+ curPos += stackedSpacing;
+ } else if (idx == firstIndex) {
+ // Bump the position of this title
+ curPos += (1.0f - alpha) * (selectedSpacing - stackedSpacing);
+
+ // The camera is glued to this position
+ float cameraPos = curPos + alpha * selectedSpacing;
+
+ // Interpolate
+ graph.xform.getTransform().setRotation(new Rotf(Vec3f.Y_AXIS, alpha * angle));
+ graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, (1.0f - alpha) * offset));
+
+ // Now recompute the position of the camera
+ // Aim to get the titles to fill a certain fraction of the vertical field of view
+ camera.setPosition(new Vec3f(cameraPos,
+ currentY,
+ currentZ));
+
+ // Maintain this much distance between the two animating titles
+ curPos += selectedSpacing;
+ } else {
+ // Interpolate
+ graph.xform.getTransform().setRotation(new Rotf(Vec3f.Y_AXIS, (1.0f - alpha) * -angle));
+ graph.xform.getTransform().setTranslation(new Vec3f(curPos, y, alpha * offset));
+
+ curPos += stackedSpacing + alpha * (selectedSpacing - stackedSpacing);
+ }
+
+ ++idx;
+ }
+
+ return true;
+ }
+
+ class MListener extends MouseAdapter {
+ RayPickAction ra = new RayPickAction();
+
+ public void mousePressed(MouseEvent e) {
+ ra.setPoint(e.getX(), e.getY(), e.getComponent());
+ // Apply to the scene root
+ ra.apply(root);
+ List<PickedPoint> pickedPoints = ra.getPickedPoints();
+ Path p = null;
+ if (!pickedPoints.isEmpty())
+ p = pickedPoints.get(0).getPath();
+ if (p != null && p.size() > 1) {
+ int idx = imageRoot.findChild(p.get(p.size() - 2));
+ if (idx >= 0) {
+ setTargetIndex(idx);
+ // Need to keep the slider and this mechanism in sync
+ // FIXME: fire an event here
+ // slider.setValue(idx);
+ }
+ }
+ }
+ }
+}