package ini.trakem2.display;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.scijava.java3d.PolygonAttributes;
import org.scijava.java3d.Transform3D;
import org.scijava.java3d.View;
import org.scijava.vecmath.Color3f;
import org.scijava.vecmath.Point3f;
import customnode.CustomLineMesh;
import customnode.CustomMesh;
import customnode.CustomMultiMesh;
import customnode.CustomTriangleMesh;
import ij.ImagePlus;
import ij.gui.GenericDialog;
import ij.measure.Calibration;
import ij3d.Content;
import ij3d.Image3DUniverse;
import ij3d.ImageWindow3D;
import ij3d.UniverseListener;
import ini.trakem2.display.d3d.ControlClickBehavior;
import ini.trakem2.display.d3d.Display3DGUI;
import ini.trakem2.imaging.PatchStack;
import ini.trakem2.tree.ProjectThing;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import ini.trakem2.vector.VectorString3D;
/** One Display3D instance for each LayerSet (maximum). */
public final class Display3D {
/** Table of LayerSet and Display3D - since there is a one to one relationship. */
static private Hashtable<LayerSet,Display3D> ht_layer_sets = new Hashtable<LayerSet,Display3D>();
/**Control calls to new Display3D. */
static private Object htlock = new Object();
/** The sky will fall on your head if you modify any of the objects contained in this table -- which is a copy of the original, but the objects are the originals. */
static public Hashtable<LayerSet,Display3D> getMasterTable() {
return new Hashtable<LayerSet,Display3D>(ht_layer_sets);
}
/** Table of ProjectThing keys versus names of Content objects in the universe. */
private Map<ProjectThing,String> ht_pt_meshes = Collections.synchronizedMap(new HashMap<ProjectThing,String>());
private Image3DUniverse universe;
private LayerSet layer_set;
/** The dimensions of the LayerSet in 2D. */
private double width, height;
private int resample = -1; // unset
static private final int DEFAULT_RESAMPLE = 4;
/** If the LayerSet dimensions are too large, then limit to max 2048 for width or height and setup a scale.*/
private final double scale = 1.0; // OBSOLETE: meshes are now generated with imglib ShapeList images.
// To fork away from the EventDispatchThread
static private ExecutorService launchers = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-launchers");
// To build meshes, or edit them
private ExecutorService executors = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-executors");
/*
static private KeyAdapter ka = new KeyAdapter() {
public void keyPressed(KeyEvent ke) {
// F1 .. F12 keys to set tools
ProjectToolbar.keyPressed(ke);
}
};
*/
/** Defaults to parallel projection. */
private Display3D(final LayerSet ls) {
this.layer_set = ls;
this.width = ls.getLayerWidth();
this.height = ls.getLayerHeight();
this.universe = new Image3DUniverse(512, 512); // size of the initial canvas, not the universe itself
this.universe.getViewer().getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); // (View.PERSPECTIVE_PROJECTION);
//this.universe.show();
final Display3DGUI gui = new Display3DGUI(this.universe);
final ImageWindow3D win = gui.init();
this.universe.init(win);
win.pack();
win.setVisible(true);
this.universe.getWindow().addWindowListener(new IW3DListener(this, ls));
this.universe.getWindow().setTitle(ls.getProject().toString() + " -- 3D Viewer");
// it ignores the listeners:
//preaddKeyListener(this.universe.getWindow(), ka);
//preaddKeyListener(this.universe.getWindow().getCanvas(), ka);
// register
Display3D.ht_layer_sets.put(ls, this);
// Add a behavior to catch control + mouse-click on
// objects in the 3D viewer and centre the front Display
// on that point:
this.universe.addInteractiveBehavior(new ControlClickBehavior(universe, ls));
this.universe.addUniverseListener(new UniverseListener() {
@Override
public void universeClosed() {
synchronized (ht_pt_meshes) {
ht_pt_meshes.clear();
}
}
@Override
public void transformationUpdated(final View arg0) {
}
@Override
public void transformationStarted(final View arg0) {
}
@Override
public void transformationFinished(final View arg0) {
}
@Override
public void contentSelected(final Content arg0) {
// TODO could select in TrakEM2's Display
}
@Override
public void contentRemoved(final Content arg0) {
final String name = arg0.getName();
synchronized (ht_pt_meshes) {
for (final Iterator<Map.Entry<ProjectThing,String>> it = ht_pt_meshes.entrySet().iterator(); it.hasNext(); ) {
if (name.equals(it.next().getValue())) {
it.remove();
break;
}
}
}
}
@Override
public void contentChanged(final Content arg0) {
}
@Override
public void contentAdded(final Content arg0) {
}
@Override
public void canvasResized() {
}
});
}
/*
private void preaddKeyListener(Component c, KeyListener kl) {
KeyListener[] all = c.getKeyListeners();
if (null != all) {
for (KeyListener k : all) c.removeKeyListener(k);
}
c.addKeyListener(kl);
if (null != all) {
for (KeyListener k : all) c.addKeyListener(k);
}
}
*/
public Image3DUniverse getUniverse() {
return universe;
}
/* Take a snapshot know-it-all mode. Each Transform3D given as argument gets assigned to the (nearly) homonimous TransformGroup, which have the following relationships:
*
* scaleTG contains rotationsTG
* rotationsTG contains translateTG
* translateTG contains centerTG
* centerTG contains the whole scene, with all meshes, etc.
*
* Any null arguments imply the current transform in the open Display3D.
*
* By default, a newly created Display3D has the scale and center transforms modified to make the scene fit nicely centered (and a bit scaled down) in the given Display3D window. The translate and rotate transforms are set to identity.
*
* The TransformGroup instances may be reached like this:
*
* LayerSet layer_set = Display.getFrontLayer().getParent();
* Display3D d3d = Display3D.getDisplay(layer_set);
* TransformGroup scaleTG = d3d.getUniverse().getGlobalScale();
* TransformGroup rotationsTG = d3d.getUniverse().getGlobalRotate();
* TransformGroup translateTG = d3d.getUniverse().getGlobalTranslate();
* TransformGroup centerTG = d3d.getUniverse().getCenterTG();
*
* ... and the Transform3D from each may be read out indirectly like this:
*
* Transform3D t_scale = new Transform3D();
* scaleTG.getTransform(t_scale);
* ...
*
* WARNING: if your java3d setup does not support offscreen rendering, the Display3D window will be brought to the front and a screen snapshot cropped to it to perform the snapshot capture. Don't cover the Display3D window with any other windows (not even an screen saver).
*
*/
/*public ImagePlus makeSnapshot(final Transform3D scale, final Transform3D rotate, final Transform3D translate, final Transform3D center) {
return universe.makeSnapshot(scale, rotate, translate, center);
}*/
/** Uses current scaling, translation and centering transforms! */
/*public ImagePlus makeSnapshotXY() { // aka posterior
// default view
return universe.makeSnapshot(null, new Transform3D(), null, null);
}*/
/** Uses current scaling, translation and centering transforms! */
/*public ImagePlus makeSnapshotXZ() { // aka dorsal
Transform3D rot1 = new Transform3D();
rot1.rotZ(-Math.PI/2);
Transform3D rot2 = new Transform3D();
rot2.rotX(Math.PI/2);
rot1.mul(rot2);
return universe.makeSnapshot(null, rot1, null, null);
}
*/
/** Uses current scaling, translation and centering transforms! */
/*
public ImagePlus makeSnapshotYZ() { // aka lateral
Transform3D rot = new Transform3D();
rot.rotY(Math.PI/2);
return universe.makeSnapshot(null, rot, null, null);
}*/
/*
public ImagePlus makeSnapshotZX() { // aka frontal
Transform3D rot = new Transform3D();
rot.rotX(-Math.PI/2);
return universe.makeSnapshot(null, rot, null, null);
}
*/
/** Uses current scaling, translation and centering transforms! Opposite side of XZ. */
/*
public ImagePlus makeSnapshotXZOpp() {
Transform3D rot1 = new Transform3D();
rot1.rotX(-Math.PI/2); // 90 degrees clockwise
Transform3D rot2 = new Transform3D();
rot2.rotY(Math.PI); // 180 degrees around Y, to the other side.
rot1.mul(rot2);
return universe.makeSnapshot(null, rot1, null, null);
}*/
private class IW3DListener extends WindowAdapter {
private Display3D d3d;
private LayerSet ls;
IW3DListener(final Display3D d3d, final LayerSet ls) {
this.d3d = d3d;
this.ls = ls;
}
@Override
public void windowClosing(final WindowEvent we) {
//Utils.log2("Display3D.windowClosing");
d3d.executors.shutdownNow();
/*Object ob =*/ ht_layer_sets.remove(ls);
/*if (null != ob) {
Utils.log2("Removed Display3D from table for LayerSet " + ls);
}*/
}
@Override
public void windowClosed(final WindowEvent we) {
//Utils.log2("Display3D.windowClosed");
ht_layer_sets.remove(ls);
}
}
static private boolean check_j3d = true;
static private boolean has_j3d_3dviewer = false;
static private boolean hasLibs() {
if (check_j3d) {
check_j3d = false;
try {
Class.forName("org.scijava.vecmath.Point3f");
has_j3d_3dviewer = true;
} catch (final ClassNotFoundException cnfe) {
Utils.log("Java 3D not installed.");
has_j3d_3dviewer = false;
return false;
}
try {
Class.forName("ij3d.ImageWindow3D");
has_j3d_3dviewer = true;
} catch (final ClassNotFoundException cnfe) {
Utils.log("3D Viewer not installed.");
has_j3d_3dviewer = false;
return false;
}
}
return has_j3d_3dviewer;
}
/** Get an existing Display3D for the given LayerSet, or create a new one for it (and cache it). */
static public Display3D get(final LayerSet ls) {
synchronized (htlock) {
try {
// test:
if (!hasLibs()) return null;
//
final Display3D d3d = ht_layer_sets.get(ls);
if (null != d3d) return d3d;
// Else, new:
final boolean[] done = new boolean[]{false};
javax.swing.SwingUtilities.invokeAndWait(new Runnable() { @Override
public void run() {
ht_layer_sets.put(ls, new Display3D(ls));
done[0] = true;
}});
// wait to avoid crashes in amd64
// try { Thread.sleep(500); } catch (Exception e) {}
while (!done[0]) {
try { Thread.sleep(10); } catch (final Exception e) {}
}
return ht_layer_sets.get(ls);
} catch (final Exception e) {
IJError.print(e);
}
}
return null;
}
/** Get the Display3D instance that exists for the given LayerSet, if any. */
static public Display3D getDisplay(final LayerSet ls) {
return ht_layer_sets.get(ls);
}
static public void setWaitingCursor() {
Utils.invokeLater(new Runnable() { @Override
public void run() {
for (final Display3D d3d : ht_layer_sets.values()) {
d3d.universe.getWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
}});
}
static public void doneWaiting() {
Utils.invokeLater(new Runnable() { @Override
public void run() {
for (final Display3D d3d : ht_layer_sets.values()) {
d3d.universe.getWindow().setCursor(Cursor.getDefaultCursor());
}
}});
}
static public Future<Vector<Future<Content>>> show(final ProjectThing pt) {
return show(pt, false, -1);
}
static public void showAndResetView(final ProjectThing pt) {
launchers.submit(new Runnable() {
@Override
public void run() {
// wait until done
final Future<Vector<Future<Content>>> fu = show(pt, true, -1);
Vector<Future<Content>> vc;
try {
vc = fu.get(); // wait until done
} catch (final Exception e) {
IJError.print(e);
return;
}
for (final Future<Content> fc : vc) {
try {
final Content c = fc.get();
if (null == c) continue;
final ArrayList<Display3D> d3ds = new ArrayList<Display3D>();
synchronized (ht_layer_sets) {
d3ds.addAll(ht_layer_sets.values());
}
/* // Disabled, it's annoying
for (Display3D d3d : d3ds) {
synchronized (d3d) {
if (d3d.universe.getContents().contains(c)) {
d3d.universe.resetView(); // reset the absolute center
d3d.universe.adjustView(); // zoom out to bring all elements in universe within view
}
}
}*/
} catch (final Exception e) {
IJError.print(e);
}
}
Utils.logAll("Reset 3D view if not within field of view!");
}
});
}
/** Scan the {@link ProjectThing} children and assign the renderable ones to an existing {@link Display3D} for their {@link LayerSet}, or open a new one. If {@code true == wait && -1 != resample}, then the method returns only when the mesh/es have been added. */
static public Future<Vector<Future<Content>>> show(final ProjectThing pt, final boolean wait, final int resample) {
if (null == pt) return null;
final Future<Vector<Future<Content>>> fu = launchers.submit(new Callable<Vector<Future<Content>>>() {
@Override
public Vector<Future<Content>> call() {
// Scan the given ProjectThing for 3D-viewable items
// So: find arealist, pipe, ball, and profile_list types
final HashSet<ProjectThing> hs = pt.findBasicTypeChildren();
if (null == hs || 0 == hs.size()) {
Utils.logAll("Node " + pt + " does not contain any 3D-displayable children");
return null;
}
// Remove profile if it lives under a profile_list
for (final Iterator<ProjectThing> it = hs.iterator(); it.hasNext(); ) {
final ProjectThing pt = it.next();
if (null != pt.getObject() && pt.getObject().getClass() == Profile.class && pt.getParent().getType().equals("profile_list")) {
it.remove();
}
}
setWaitingCursor();
// Start new scheduler to publish/add meshes to the 3D Viewer every 5 seconds and when done.
final Hashtable<Display3D,Vector<Content>> contents = new Hashtable<Display3D,Vector<Content>>();
final ScheduledExecutorService updater = Executors.newScheduledThreadPool(1);
final AtomicInteger counter = new AtomicInteger();
updater.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// Obtain a copy of the contents queue
final HashMap<Display3D,Vector<Content>> m = new HashMap<Display3D,Vector<Content>>();
synchronized (contents) {
m.putAll(contents);
contents.clear();
}
if (m.isEmpty()) return;
// Add all to the corresponding Display3D
for (final Map.Entry<Display3D,Vector<Content>> e : m.entrySet()) {
e.getKey().universe.addContentLater(e.getValue());
counter.getAndAdd(e.getValue().size());
}
Utils.showStatus(new StringBuilder("Rendered ").append(counter.get()).append('/').append(hs.size()).toString());
}
}, 100, 4000, TimeUnit.MILLISECONDS);
// A list of all generated Content objects
final Vector<Future<Content>> list = new Vector<Future<Content>>();
for (final Iterator<ProjectThing> it = hs.iterator(); it.hasNext(); ) {
// obtain the Displayable object under the node
final ProjectThing child = it.next();
final Object obc = child.getObject();
final Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
if (null != displ) {
if (displ.getClass().equals(Profile.class)) {
//Utils.log("Display3D can't handle Bezier profiles at the moment.");
// handled by profile_list Thing
continue;
}
if (!displ.isVisible()) {
Utils.log("Skipping non-visible node " + displ);
continue;
}
}
// obtain the containing LayerSet
final Display3D d3d;
if (null != displ) d3d = Display3D.get(displ.getLayerSet());
else if (child.getType().equals("profile_list")) {
final ArrayList<ProjectThing> al_children = child.getChildren();
if (null == al_children || 0 == al_children.size()) continue;
// else, get the first Profile and get its LayerSet
d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
} else {
Utils.log("Don't know what to do with node " + child);
d3d = null;
}
if (null == d3d) {
Utils.log("Could not get a proper 3D display for node " + displ);
return null; // java3D not installed most likely
}
boolean already;
synchronized (d3d.ht_pt_meshes) {
already = d3d.ht_pt_meshes.containsKey(child);
}
if (already) {
if (child.getObject() instanceof ZDisplayable) {
Utils.log("Updating 3D view of " + child.getObject());
} else {
Utils.log("Updating 3D view of " + child);
}
}
list.add(d3d.executors.submit(new Callable<Content>() {
@Override
public Content call() {
Content c = null;
try {
c = d3d.createMesh(child, displ, resample).call();
Vector<Content> vc;
synchronized (contents) {
vc = contents.get(d3d);
if (null == vc) vc = new Vector<Content>();
contents.put(d3d, vc);
}
vc.add(c);
} catch (final Exception e) {
IJError.print(e);
}
return c;
}
}));
// If it's the last one:
if (!it.hasNext()) {
// Add the concluding task, that waits on all and shuts down the scheduler
d3d.executors.submit(new Runnable() {
@Override
public void run() {
// Wait until all are done
for (final Future<Content> c : list) {
try {
c.get();
} catch (final Throwable t) {
IJError.print(t);
}
}
try {
// Shutdown scheduler and execute remaining tasks
for (final Runnable r : updater.shutdownNow()) {
r.run();
}
} catch (final Throwable e) {
IJError.print(e);
}
// Reset cursor
doneWaiting();
Utils.showStatus(new StringBuilder("Done rendering ").append(counter.get()).append('/').append(hs.size()).toString());
}
});
}
}
return list;
}});
if (wait && -1 != resample) {
try {
fu.get();
} catch (final Throwable t) {
IJError.print(t);
}
}
return fu;
}
static public void resetView(final LayerSet ls) {
final Display3D d3d = ht_layer_sets.get(ls);
if (null != d3d) d3d.universe.resetView();
}
static public void showOrthoslices(final Patch p) {
final Display3D d3d = get(p.getLayerSet());
d3d.adjustResampling();
//d3d.universe.resetView();
final String title = makeTitle(p) + " orthoslices";
// remove if present
d3d.universe.removeContent(title);
final PatchStack ps = p.makePatchStack();
final ImagePlus imp = get8BitStack(ps);
final Content ct = d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
setTransform(ct, ps.getPatch(0));
ct.setLocked(true); // locks the added content
}
static public void showVolume(final Patch p) {
final Display3D d3d = get(p.getLayerSet());
d3d.adjustResampling();
//d3d.universe.resetView();
final String title = makeTitle(p) + " volume";
// remove if present
d3d.universe.removeContent(title);
final PatchStack ps = p.makePatchStack();
final ImagePlus imp = get8BitStack(ps);
final Content ct = d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
setTransform(ct, ps.getPatch(0));
ct.setLocked(true); // locks the added content
}
static private void setTransform(final Content ct, final Patch p) {
final double[] a = new double[6];
p.getAffineTransform().getMatrix(a);
final Calibration cal = p.getLayerSet().getCalibration();
// a is: m00 m10 m01 m11 m02 m12
// d expects: m01 m02 m03 m04, m11 m12 ...
ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
a[1], a[3], 0, a[5] * cal.pixelWidth,
0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
0, 0, 0, 1}));
}
static public void showOrthoslices(final ImagePlus imp, final String title, final int wx, final int wy, final float scale2D, final Layer first) {
final Display3D d3d = get(first.getParent());
d3d.universe.removeContent(title);
final Content ct = d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, 1);
final Calibration cal = imp.getCalibration();
final Transform3D t = new Transform3D(new double[]{1, 0, 0, wx * cal.pixelWidth * scale2D,
0, 1, 0, wy * cal.pixelHeight * scale2D,
0, 0, scale2D, first.getZ() * cal.pixelWidth * scale2D, // not pixelDepth!
0, 0, 0, 1});
// why scale2D has to be there at all reflects a horrible underlying setting of the calibration, plus of the scaling in the Display3D.
Utils.log(t);
ct.applyTransform(t);
ct.setLocked(true);
}
/** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
* If the PatchStack is already of the right type, it is returned,
* otherwise a copy is made in the proper type.
*/
static private ImagePlus get8BitStack(final PatchStack ps) {
switch (ps.getType()) {
case ImagePlus.COLOR_RGB:
// convert stack to 8-bit color
return ps.createColor256Copy();
case ImagePlus.GRAY16:
case ImagePlus.GRAY32:
// convert stack to 8-bit
return ps.createGray8Copy();
case ImagePlus.GRAY8:
case ImagePlus.COLOR_256:
return ps;
default:
Utils.logAll("Cannot handle stacks of type: " + ps.getType());
return null;
}
}
/** Considers there is only one Display3D for each LayerSet. */
static public void remove(final ProjectThing pt) {
if (null == pt) return;
if (null == pt.getObject()) return;
final Object ob = pt.getObject();
if (!(ob instanceof Displayable)) return;
final Displayable displ = (Displayable)ob;
final Display3D d3d = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
if (null == d3d) {
// there is no Display3D showing the pt to remove
//Utils.log2("No Display3D contains ProjectThing: " + pt);
return;
}
String name;
synchronized (d3d.ht_pt_meshes) {
name = d3d.ht_pt_meshes.remove(pt);
}
if (null == name) {
Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
return;
}
d3d.universe.removeContent(name);
}
/** Creates a mesh for the given Displayable in a separate Thread, and adds it to the universe. */
private Future<Content> addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
return executors.submit(new Callable<Content>() {
@Override
public Content call() {
try {
// 1 - Create content
final Callable<Content> c = createMesh(pt, displ, resample);
if (null == c) return null;
final Content content = c.call();
if (null == content) return null;
final String title = content.getName();
// 2 - Remove from universe any content of the same title
if (universe.contains(title)) {
universe.removeContent(title);
}
// 3 - Add to universe, and wait
universe.addContentLater(content).get();
return content;
} catch (final Exception e) {
IJError.print(e);
return null;
}
}
});
}
static private final String makeProfileListTitle(final ProjectThing pt) {
String title;
final Object ob = pt.getParent().getTitle();
if (null == ob || ob.equals(pt.getParent().getType())) title = pt.toString() + " #" + pt.getId(); // Project.getMeaningfulTitle can't handle profile_list properly
else title = ob.toString() + " /[" + pt.getParent().getType() + "]/[profile_list] #" + pt.getId();
return title;
}
/** Remove all basic type children contained in {@code pt} and its children, recursively.
*
* @param pt
*/
static public void removeFrom3D(final ProjectThing pt) {
final HashSet<ProjectThing> hs = pt.findBasicTypeChildren();
if (null == hs || 0 == hs.size()) {
Utils.logAll("Nothing to remove from 3D.");
return;
}
// Ignore Profile instances ("profile_list" takes care of them)
for (final ProjectThing child : hs) {
if (child.getByType().equals("profile")) continue;
// Find the LayerSet
LayerSet lset = null;
if (child.getType().equals("profile_list")) {
if (!child.hasChildren()) continue;
for (final ProjectThing p : child.getChildren()) {
if (null != p.getObject() && p.getObject() instanceof Profile) {
lset = ((Displayable)p.getObject()).getLayerSet();
break;
}
}
if (null == lset) continue;
} else if (child.getType().equals("profile")) {
// Taken care of by "profile list"
continue;
} else {
final Displayable d = (Displayable)child.getObject();
if (null == d) {
Utils.log("Null object for ProjectThing " + child);
continue;
}
lset = d.getLayerSet();
}
if (null == lset) {
Utils.log("No LayerSet found for " + child);
continue;
}
final Display3D d3d = getDisplay(lset);
if (null == d3d) {
Utils.log("No Display 3D found for " + child);
continue; // no Display3D window open
}
final String oldTitle = d3d.ht_pt_meshes.remove(child);
if (null == oldTitle) {
Utils.log("Could not find a title for " + child);
continue;
}
Utils.log("Removed from 3D view: " + oldTitle);
d3d.getUniverse().removeContent(oldTitle);
}
}
/** Returns a function that returns a Content object.
* Does NOT add the Content to the universe; it merely creates it. */
public Callable<Content> createMesh(final ProjectThing pt, final Displayable displ, final int resample) {
final double scale = 1.0; // OBSOLETE
return new Callable<Content>() {
@Override
public Content call() {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
try {
// the list 'triangles' is really a list of Point3f, which define a triangle every 3 consecutive points. (TODO most likely Bene Schmid got it wrong: I don't think there's any need to have the points duplicated if they overlap in space but belong to separate triangles.)
final List<Point3f> triangles;
//boolean no_culling_ = false;
final Class<?> c;
final boolean line_mesh;
final int line_mesh_mode;
if (null == displ) {
c = null;
line_mesh = false;
line_mesh_mode = Integer.MAX_VALUE;
} else {
c = displ.getClass();
line_mesh = Tree.class.isAssignableFrom(c) || Polyline.class == c;
if (Tree.class.isAssignableFrom(c)) line_mesh_mode = CustomLineMesh.PAIRWISE;
else if (Polyline.class == c) line_mesh_mode = CustomLineMesh.CONTINUOUS;
else line_mesh_mode = Integer.MAX_VALUE; // disabled
}
List<Point3f> extra_triangles = null;
List<Color3f> triangle_colors = null,
extra_triangle_colors = null;
int rs = resample;
if (displ instanceof AreaContainer) {
if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
else rs = Display3D.this.resample;
}
if (AreaList.class == c) {
triangles = ((AreaList)displ).generateTriangles(scale, rs);
//triangles = removeNonManifold(triangles);
} else if (Ball.class == c) {
final double[][][] globe = Ball.generateGlobe(12, 12);
triangles = ((Ball)displ).generateTriangles(scale, globe);
} else if (displ instanceof Line3D) {
// Pipe and Polyline
// adjustResampling(); // fails horribly, needs first to correct mesh-generation code
triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
} else if (displ instanceof Tree<?>) {
// A 3D wire skeleton, using CustomLineMesh
final Tree.MeshData skeleton = ((Tree<?>)displ).generateSkeleton(scale, 12, 1);
triangles = skeleton.verts;
triangle_colors = skeleton.colors;
if (displ instanceof Treeline) {
final Tree.MeshData tube = ((Treeline)displ).generateMesh(scale, 12);
extra_triangles = tube.verts;
extra_triangle_colors = tube.colors;
} else if (displ instanceof AreaTree) {
final Tree.MeshData mesh = ((AreaTree)displ).generateMesh(scale, rs);
extra_triangles = mesh.verts;
extra_triangle_colors = mesh.colors;
}
if (null != extra_triangles && extra_triangles.isEmpty()) extra_triangles = null; // avoid issues with MultiMesh
} else if (Connector.class == c) {
final Tree.MeshData octopus = ((Connector)displ).generateMesh(scale, 12);
triangles = octopus.verts;
triangle_colors = octopus.colors;
} else if (null == displ && pt.getType().equals("profile_list")) {
triangles = Profile.generateTriangles(pt, scale);
//no_culling_ = true;
} else {
Utils.log("Unrecognized type for 3D mesh generation: " + (null != displ ? displ.getClass() : null) + " : " + displ);
triangles = null;
}
// safety checks
if (null == triangles) {
Utils.log("Some error ocurred: can't create triangles for " + displ);
return null;
}
if (0 == triangles.size()) {
Utils.log2("Skipping empty mesh for " + displ.getTitle());
return null;
}
if (!line_mesh && 0 != triangles.size() % 3) {
Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
return null;
}
final Color color;
final float alpha;
final String title;
if (null != displ) {
color = displ.getColor();
alpha = displ.getAlpha();
title = makeTitle(displ);
} else if (pt.getType().equals("profile_list")) {
// for profile_list: get from the first (what a kludge; there should be a ZDisplayable ProfileList object)
final Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
if (null == obp) return null;
final Displayable di = (Displayable)obp;
color = di.getColor();
alpha = di.getAlpha();
title = makeProfileListTitle(pt);
} else {
title = pt.toString() + " #" + pt.getId();
color = null;
alpha = 1.0f;
}
// Why for all? Above no_culling_ is set to true or false, depending upon type. --> Because with transparencies it looks proper and better when no_culling is true.
final boolean no_culling = true; // for ALL
Content ct = null;
try {
final Color3f c3 = new Color3f(color);
// If it exists, remove and add as new:
universe.removeContent(title);
final CustomMesh cm;
if (line_mesh) {
//ct = universe.createContent(new CustomLineMesh(triangles, line_mesh_mode, c3, 0), title);
cm = new CustomLineMesh(triangles, line_mesh_mode, c3, 0);
} else if (no_culling) {
// create a mesh with the same color and zero transparency (that is, full opacity)
final CustomTriangleMesh mesh = new CustomTriangleMesh(triangles, c3, 0);
// Set mesh properties for double-sided triangles
final PolygonAttributes pa = mesh.getAppearance().getPolygonAttributes();
pa.setCullFace(PolygonAttributes.CULL_NONE);
pa.setBackFaceNormalFlip(true);
mesh.setColor(c3);
// After setting properties, add to the viewer
//ct = universe.createContent(mesh, title);
cm = mesh;
} else {
//ct = universe.createContent(new CustomTriangleMesh(triangles, c3, 0), title);
cm = new CustomTriangleMesh(triangles, c3, 0);
}
if (null != triangle_colors) cm.setColor(triangle_colors);
//if (null == cm) return null;
if (null == extra_triangles || 0 == extra_triangles.size()) {
ct = universe.createContent(cm, title);
} else {
final CustomTriangleMesh extra = new CustomTriangleMesh(extra_triangles, c3, 0);
if (null != extra_triangle_colors) {
// Set mesh properties for double-sided triangles
final PolygonAttributes pa = extra.getAppearance().getPolygonAttributes();
pa.setCullFace(PolygonAttributes.CULL_NONE);
pa.setBackFaceNormalFlip(true);
extra.setColor(extra_triangle_colors);
}
ct = universe.createContent(new CustomMultiMesh(Arrays.asList(new CustomMesh[]{cm, extra})), title);
}
// Set general content properties
ct.setTransparency(1f - alpha);
// Default is unlocked (editable) transformation; set it to locked:
ct.setLocked(true);
// register mesh title
synchronized (ht_pt_meshes) {
ht_pt_meshes.put(pt, ct.getName());
}
} catch (final Throwable e) {
Utils.logAll("Mesh generation failed for \"" + title + "\" from " + pt);
IJError.print(e);
e.printStackTrace();
}
Utils.log2(pt.toString() + " n points: " + triangles.size());
return ct;
} catch (final Exception e) {
IJError.print(e);
return null;
}
}};
}
static public class VectorStringContent {
VectorString3D vs;
String title;
Color color;
double[] widths;
float alpha;
public VectorStringContent(final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha){
this.vs = vs;
this.title = title;
this.color = color;
this.widths = widths;
this.alpha = alpha;
}
public Content asContent(final Display3D d3d) {
double[] wi = widths;
if (null == widths) {
wi = new double[vs.getPoints(0).length];
Arrays.fill(wi, 2.0);
} else if (widths.length != vs.length()) {
Utils.log("ERROR: widths.length != VectorString3D.length()");
return null;
}
float transp = 1 - alpha;
if (transp < 0) transp = 0;
if (transp > 1) transp = 1;
if (1 == transp) {
Utils.log("WARNING: adding a 3D object fully transparent.");
}
final List<Point3f> triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), d3d.scale);
final Content ct = d3d.universe.createContent(new CustomTriangleMesh(triangles, new Color3f(color), 0), title);
ct.setTransparency(transp);
ct.setLocked(true);
return ct;
}
}
/** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
static public Future<Collection<Future<Content>>> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
return addMesh(ref_ls, vs, title, color, null, 1.0f);
}
/** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
static public Future<Collection<Future<Content>>> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha) {
final List<Content> col = new ArrayList<Content>();
final Display3D d3d = Display3D.get(ref_ls);
col.add(new VectorStringContent(vs, title, color, widths, alpha).asContent(d3d));
return d3d.addContent(col);
}
static public Future<Collection<Future<Content>>> show(final LayerSet ref_ls, final Collection<Content> col) {
final Display3D d3d = get(ref_ls);
return d3d.addContent(col);
}
public Future<Collection<Future<Content>>> addContent(final Collection<Content> col) {
final FutureTask<Collection<Future<Content>>> fu = new FutureTask<Collection<Future<Content>>>(new Callable<Collection<Future<Content>>>() {
@Override
public Collection<Future<Content>> call() {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
try {
return universe.addContentLater(col);
} catch (final Throwable e) {
IJError.print(e);
return null;
}
}});
launchers.submit(new Runnable() { @Override
public void run() {
executors.submit(fu);
}});
return fu;
}
public Future<Content> addContent(final Content c) {
final FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
@Override
public Content call() {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
try {
return universe.addContentLater(c).get();
} catch (final Throwable e) {
IJError.print(e);
return null;
}
}
});
launchers.submit(new Runnable() { @Override
public void run() {
executors.submit(fu);
}});
return fu;
}
static public final int estimateResamplingFactor(final LayerSet ls, final double width, final double height) {
final int max_dimension = ls.getPixelsMaxDimension();
return (int)(DEFAULT_RESAMPLE / (Math.max(width, height) > max_dimension ?
max_dimension / Math.max(width, height)
: 1));
}
/** Estimate a scaling factor, to be used as a multiplier of the suggested default resampling. */
private final int estimateResamplingFactor() {
return estimateResamplingFactor(layer_set, width, height);
}
// This method has the exclusivity in adjusting the resampling value, and it also returns it.
synchronized private final int adjustResampling() {
if (resample > 0) return resample;
final GenericDialog gd = new GenericDialog("Resample");
final int default_resample = estimateResamplingFactor();
gd.addSlider("Resample: ", 1, Math.max(default_resample, 100), -1 != resample ? resample : default_resample);
gd.showDialog();
if (gd.wasCanceled()) {
resample = -1 != resample ? resample : default_resample; // current or default value
return resample;
}
resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
return resample;
}
/** Checks if there is any Display3D instance currently showing the given Displayable. */
static public boolean isDisplayed(final Displayable d) {
if (null == d) return false;
final String title = makeTitle(d);
for (final Display3D d3d : ht_layer_sets.values()) {
if (null != d3d.universe.getContent(title)) return true;
}
if (d.getClass() == Profile.class) {
if (null != getProfileContent(d)) return true;
}
return false;
}
/** Checks if the given Displayable is a Profile, and tries to find a possible Content object in the Image3DUniverse of its LayerSet according to the title as created from its profile_list ProjectThing. */
static public Content getProfileContent(final Displayable d) {
if (null == d) return null;
if (d.getClass() != Profile.class) return null;
final Display3D d3d = get(d.getLayer().getParent());
if (null == d3d) return null;
ProjectThing pt = d.getProject().findProjectThing(d);
if (null == pt) return null;
pt = (ProjectThing) pt.getParent();
return d3d.universe.getContent(new StringBuilder(pt.toString()).append(" #").append(pt.getId()).toString());
}
static public Future<Boolean> setColor(final Displayable d, final Color color) {
final Display3D d3d = getDisplay(d.getLayer().getParent());
if (null == d3d) return null; // no 3D displays open
return d3d.executors.submit(new Callable<Boolean>() { @Override
public Boolean call() {
Content content = d3d.universe.getContent(makeTitle(d));
if (null == content) content = getProfileContent(d);
if (null != content) {
content.setColor(new Color3f(color));
return true;
}
return false;
}});
}
static public Future<Boolean> setTransparency(final Displayable d, final float alpha) {
if (null == d) return null;
final Layer layer = d.getLayer();
if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
final Display3D d3d = ht_layer_sets.get(layer.getParent());
if (null == d3d) return null;
return d3d.executors.submit(new Callable<Boolean>() { @Override
public Boolean call() {
String title = makeTitle(d);
Content content = d3d.universe.getContent(title);
if (null == content) content = getProfileContent(d);
if (null != content) content.setTransparency(1 - alpha);
else if (null == content && d.getClass().equals(Patch.class)) {
final Patch pa = (Patch)d;
if (pa.isStack()) {
title = pa.getProject().getLoader().getFileName(pa);
for (final Display3D dd : ht_layer_sets.values()) {
for (final Iterator<?> cit = dd.universe.getContents().iterator(); cit.hasNext(); ) {
final Content c = (Content)cit.next();
if (c.getName().startsWith(title)) {
c.setTransparency(1 - alpha);
// no break, since there could be a volume and an orthoslice
}
}
}
}
}
return true;
}});
}
static public String makeTitle(final Displayable d) {
return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
}
static public String makeTitle(final Patch p) {
return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
+ " #" + p.getProject().getLoader().getNextId();
}
/** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
* (otherwise returns null). */
static public Future<Content> update(final Displayable d) {
final Layer layer = d.getLayer();
if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
final Display3D d3d = ht_layer_sets.get(layer.getParent());
if (null == d3d) return null;
return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
}
/*
static public final double computeTriangleArea() {
return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
Math.pow(yA*zB + yB*zC + yC*zA, 2) +
Math.pow(zA*xB + zB*xC + zC*xA, 2));
}
*/
static public final boolean contains(final LayerSet ls, final String title) {
final Display3D d3d = getDisplay(ls);
if (null == d3d) return false;
return null != d3d.universe.getContent(title);
}
static public void destroy() {
launchers.shutdownNow();
}
static public void init() {
if (launchers.isShutdown()) {
launchers = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-launchers");
}
}
/** Creates a calibrated sphere to represent a point at LayerSet pixel coordinates wx, wy, wz, with radius wr.*/
public List<Point3f> createFatPoint(final double wx, final double wy, final double wz, final double wr, final Calibration cal) {
final double[][][] globe = Ball.generateGlobe(12, 12);
final int sign = cal.pixelDepth < 0 ? -1 : 1;
for (int z=0; z<globe.length; z++) {
for (int k=0; k<globe[0].length; k++) {
globe[z][k][0] = (globe[z][k][0] * wr + wx) * scale * cal.pixelWidth;
globe[z][k][1] = (globe[z][k][1] * wr + wy) * scale * cal.pixelHeight;
globe[z][k][2] = (globe[z][k][2] * wr + wz) * scale * cal.pixelWidth * sign; // not pixelDepth, see day notes 20080227. Because pixelDepth is in microns/px, not in px/microns, and the z coord here is taken from the z of the layer, which is in pixels.
}
}
final ArrayList<Point3f> list = new ArrayList<Point3f>();
// create triangular faces and add them to the list
for (int z=0; z<globe.length-1; z++) { // the parallels
for (int k=0; k<globe[0].length -1; k++) { // meridian points
// half quadrant (a triangle)
list.add(new Point3f((float)globe[z][k][0], (float)globe[z][k][1], (float)globe[z][k][2]));
list.add(new Point3f((float)globe[z+1][k+1][0], (float)globe[z+1][k+1][1], (float)globe[z+1][k+1][2]));
list.add(new Point3f((float)globe[z+1][k][0], (float)globe[z+1][k][1], (float)globe[z+1][k][2]));
// the other half quadrant
list.add(new Point3f((float)globe[z][k][0], (float)globe[z][k][1], (float)globe[z][k][2]));
list.add(new Point3f((float)globe[z][k+1][0], (float)globe[z][k+1][1], (float)globe[z][k+1][2]));
list.add(new Point3f((float)globe[z+1][k+1][0], (float)globe[z+1][k+1][1], (float)globe[z+1][k+1][2]));
}
}
return list;
}
/** Expects uncalibrated wx,wy,wz, (i.e. pixel values), to be calibrated by @param ls calibration. */
static public final Future<Content> addFatPoint(final String title, final LayerSet ls, final double wx, final double wy, final double wz, final double wr, final Color color) {
final Display3D d3d = Display3D.get(ls);
d3d.universe.removeContent(title);
final Content ct = d3d.universe.createContent(new CustomTriangleMesh(d3d.createFatPoint(wx, wy, wz, wr, ls.getCalibrationCopy()), new Color3f(color), 0), title);
ct.setLocked(true);
return d3d.addContent(ct);
}
}