Use internal SNAPSHOT couplings again
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / Display3D.java
blobb44ec82c2577e393d861362b7af12159b6be0c45
1 package ini.trakem2.display;
3 import customnode.CustomLineMesh;
4 import customnode.CustomMesh;
5 import customnode.CustomMultiMesh;
6 import customnode.CustomTriangleMesh;
7 import ij.ImagePlus;
8 import ij.gui.GenericDialog;
9 import ij.measure.Calibration;
10 import ij3d.Content;
11 import ij3d.Image3DUniverse;
12 import ij3d.ImageWindow3D;
13 import ij3d.UniverseListener;
14 import ini.trakem2.display.d3d.ControlClickBehavior;
15 import ini.trakem2.display.d3d.Display3DGUI;
16 import ini.trakem2.imaging.PatchStack;
17 import ini.trakem2.tree.ProjectThing;
18 import ini.trakem2.utils.IJError;
19 import ini.trakem2.utils.Utils;
20 import ini.trakem2.vector.VectorString3D;
22 import java.awt.Color;
23 import java.awt.Cursor;
24 import java.awt.event.WindowAdapter;
25 import java.awt.event.WindowEvent;
26 import java.io.File;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Hashtable;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Vector;
38 import java.util.concurrent.Callable;
39 import java.util.concurrent.ExecutorService;
40 import java.util.concurrent.Executors;
41 import java.util.concurrent.Future;
42 import java.util.concurrent.FutureTask;
43 import java.util.concurrent.ScheduledExecutorService;
44 import java.util.concurrent.TimeUnit;
45 import java.util.concurrent.atomic.AtomicInteger;
47 import javax.media.j3d.PolygonAttributes;
48 import javax.media.j3d.Transform3D;
49 import javax.media.j3d.View;
50 import javax.vecmath.Color3f;
51 import javax.vecmath.Point3f;
54 /** One Display3D instance for each LayerSet (maximum). */
55 public final class Display3D {
57 /** Table of LayerSet and Display3D - since there is a one to one relationship. */
58 static private Hashtable<LayerSet,Display3D> ht_layer_sets = new Hashtable<LayerSet,Display3D>();
59 /**Control calls to new Display3D. */
60 static private Object htlock = new Object();
62 /** 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. */
63 static public Hashtable<LayerSet,Display3D> getMasterTable() {
64 return new Hashtable<LayerSet,Display3D>(ht_layer_sets);
67 /** Table of ProjectThing keys versus names of Content objects in the universe. */
68 private Map<ProjectThing,String> ht_pt_meshes = Collections.synchronizedMap(new HashMap<ProjectThing,String>());
70 private Image3DUniverse universe;
72 private LayerSet layer_set;
73 /** The dimensions of the LayerSet in 2D. */
74 private double width, height;
75 private int resample = -1; // unset
76 static private final int DEFAULT_RESAMPLE = 4;
77 /** If the LayerSet dimensions are too large, then limit to max 2048 for width or height and setup a scale.*/
78 private final double scale = 1.0; // OBSOLETE: meshes are now generated with imglib ShapeList images.
81 // To fork away from the EventDispatchThread
82 static private ExecutorService launchers = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-launchers");
84 // To build meshes, or edit them
85 private ExecutorService executors = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-executors");
88 static private KeyAdapter ka = new KeyAdapter() {
89 public void keyPressed(KeyEvent ke) {
90 // F1 .. F12 keys to set tools
91 ProjectToolbar.keyPressed(ke);
96 /** Defaults to parallel projection. */
97 private Display3D(final LayerSet ls) {
98 this.layer_set = ls;
99 this.width = ls.getLayerWidth();
100 this.height = ls.getLayerHeight();
101 this.universe = new Image3DUniverse(512, 512); // size of the initial canvas, not the universe itself
102 this.universe.getViewer().getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); // (View.PERSPECTIVE_PROJECTION);
104 //this.universe.show();
106 Display3DGUI gui = new Display3DGUI(this.universe);
107 ImageWindow3D win = gui.init();
108 this.universe.init(win);
109 win.pack();
110 win.setVisible(true);
112 this.universe.getWindow().addWindowListener(new IW3DListener(this, ls));
113 this.universe.getWindow().setTitle(ls.getProject().toString() + " -- 3D Viewer");
114 // it ignores the listeners:
115 //preaddKeyListener(this.universe.getWindow(), ka);
116 //preaddKeyListener(this.universe.getWindow().getCanvas(), ka);
118 // register
119 Display3D.ht_layer_sets.put(ls, this);
121 // Add a behavior to catch control + mouse-click on
122 // objects in the 3D viewer and centre the front Display
123 // on that point:
124 this.universe.addInteractiveBehavior(new ControlClickBehavior(universe, ls));
126 this.universe.addUniverseListener(new UniverseListener() {
127 @Override
128 public void universeClosed() {
129 synchronized (ht_pt_meshes) {
130 ht_pt_meshes.clear();
133 @Override
134 public void transformationUpdated(View arg0) {
136 @Override
137 public void transformationStarted(View arg0) {
139 @Override
140 public void transformationFinished(View arg0) {
142 @Override
143 public void contentSelected(Content arg0) {
144 // TODO could select in TrakEM2's Display
146 @Override
147 public void contentRemoved(Content arg0) {
148 String name = arg0.getName();
149 synchronized (ht_pt_meshes) {
150 for (final Iterator<Map.Entry<ProjectThing,String>> it = ht_pt_meshes.entrySet().iterator(); it.hasNext(); ) {
151 if (name.equals(it.next().getValue())) {
152 it.remove();
153 break;
158 @Override
159 public void contentChanged(Content arg0) {
161 @Override
162 public void contentAdded(Content arg0) {
164 @Override
165 public void canvasResized() {
171 private void preaddKeyListener(Component c, KeyListener kl) {
172 KeyListener[] all = c.getKeyListeners();
173 if (null != all) {
174 for (KeyListener k : all) c.removeKeyListener(k);
176 c.addKeyListener(kl);
177 if (null != all) {
178 for (KeyListener k : all) c.addKeyListener(k);
183 public Image3DUniverse getUniverse() {
184 return universe;
187 /* Take a snapshot know-it-all mode. Each Transform3D given as argument gets assigned to the (nearly) homonimous TransformGroup, which have the following relationships:
189 * scaleTG contains rotationsTG
190 * rotationsTG contains translateTG
191 * translateTG contains centerTG
192 * centerTG contains the whole scene, with all meshes, etc.
194 * Any null arguments imply the current transform in the open Display3D.
196 * 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.
198 * The TransformGroup instances may be reached like this:
200 * LayerSet layer_set = Display.getFrontLayer().getParent();
201 * Display3D d3d = Display3D.getDisplay(layer_set);
202 * TransformGroup scaleTG = d3d.getUniverse().getGlobalScale();
203 * TransformGroup rotationsTG = d3d.getUniverse().getGlobalRotate();
204 * TransformGroup translateTG = d3d.getUniverse().getGlobalTranslate();
205 * TransformGroup centerTG = d3d.getUniverse().getCenterTG();
207 * ... and the Transform3D from each may be read out indirectly like this:
209 * Transform3D t_scale = new Transform3D();
210 * scaleTG.getTransform(t_scale);
211 * ...
213 * 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).
216 /*public ImagePlus makeSnapshot(final Transform3D scale, final Transform3D rotate, final Transform3D translate, final Transform3D center) {
217 return universe.makeSnapshot(scale, rotate, translate, center);
220 /** Uses current scaling, translation and centering transforms! */
221 /*public ImagePlus makeSnapshotXY() { // aka posterior
222 // default view
223 return universe.makeSnapshot(null, new Transform3D(), null, null);
225 /** Uses current scaling, translation and centering transforms! */
226 /*public ImagePlus makeSnapshotXZ() { // aka dorsal
227 Transform3D rot1 = new Transform3D();
228 rot1.rotZ(-Math.PI/2);
229 Transform3D rot2 = new Transform3D();
230 rot2.rotX(Math.PI/2);
231 rot1.mul(rot2);
232 return universe.makeSnapshot(null, rot1, null, null);
235 /** Uses current scaling, translation and centering transforms! */
237 public ImagePlus makeSnapshotYZ() { // aka lateral
238 Transform3D rot = new Transform3D();
239 rot.rotY(Math.PI/2);
240 return universe.makeSnapshot(null, rot, null, null);
244 public ImagePlus makeSnapshotZX() { // aka frontal
245 Transform3D rot = new Transform3D();
246 rot.rotX(-Math.PI/2);
247 return universe.makeSnapshot(null, rot, null, null);
251 /** Uses current scaling, translation and centering transforms! Opposite side of XZ. */
253 public ImagePlus makeSnapshotXZOpp() {
254 Transform3D rot1 = new Transform3D();
255 rot1.rotX(-Math.PI/2); // 90 degrees clockwise
256 Transform3D rot2 = new Transform3D();
257 rot2.rotY(Math.PI); // 180 degrees around Y, to the other side.
258 rot1.mul(rot2);
259 return universe.makeSnapshot(null, rot1, null, null);
262 private class IW3DListener extends WindowAdapter {
263 private Display3D d3d;
264 private LayerSet ls;
265 IW3DListener(Display3D d3d, LayerSet ls) {
266 this.d3d = d3d;
267 this.ls = ls;
269 public void windowClosing(WindowEvent we) {
270 //Utils.log2("Display3D.windowClosing");
271 d3d.executors.shutdownNow();
272 /*Object ob =*/ ht_layer_sets.remove(ls);
273 /*if (null != ob) {
274 Utils.log2("Removed Display3D from table for LayerSet " + ls);
277 public void windowClosed(WindowEvent we) {
278 //Utils.log2("Display3D.windowClosed");
279 ht_layer_sets.remove(ls);
283 static private boolean check_j3d = true;
284 static private boolean has_j3d_3dviewer = false;
286 static private boolean hasLibs() {
287 if (check_j3d) {
288 check_j3d = false;
289 try {
290 Class.forName("javax.vecmath.Point3f");
291 has_j3d_3dviewer = true;
292 } catch (ClassNotFoundException cnfe) {
293 Utils.log("Java 3D not installed.");
294 has_j3d_3dviewer = false;
295 return false;
297 try {
298 Class.forName("ij3d.ImageWindow3D");
299 has_j3d_3dviewer = true;
300 } catch (ClassNotFoundException cnfe) {
301 Utils.log("3D Viewer not installed.");
302 has_j3d_3dviewer = false;
303 return false;
306 return has_j3d_3dviewer;
309 /** Get an existing Display3D for the given LayerSet, or create a new one for it (and cache it). */
310 static public Display3D get(final LayerSet ls) {
311 synchronized (htlock) {
312 try {
313 // test:
314 if (!hasLibs()) return null;
316 Display3D d3d = ht_layer_sets.get(ls);
317 if (null != d3d) return d3d;
318 // Else, new:
319 final boolean[] done = new boolean[]{false};
320 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() {
321 ht_layer_sets.put(ls, new Display3D(ls));
322 done[0] = true;
323 }});
324 // wait to avoid crashes in amd64
325 // try { Thread.sleep(500); } catch (Exception e) {}
326 while (!done[0]) {
327 try { Thread.sleep(10); } catch (Exception e) {}
329 return ht_layer_sets.get(ls);
330 } catch (Exception e) {
331 IJError.print(e);
334 return null;
337 /** Get the Display3D instance that exists for the given LayerSet, if any. */
338 static public Display3D getDisplay(final LayerSet ls) {
339 return ht_layer_sets.get(ls);
342 static public void setWaitingCursor() {
343 Utils.invokeLater(new Runnable() { public void run() {
344 for (Display3D d3d : ht_layer_sets.values()) {
345 d3d.universe.getWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
347 }});
350 static public void doneWaiting() {
351 Utils.invokeLater(new Runnable() { public void run() {
352 for (Display3D d3d : ht_layer_sets.values()) {
353 d3d.universe.getWindow().setCursor(Cursor.getDefaultCursor());
355 }});
358 static public Future<Vector<Future<Content>>> show(ProjectThing pt) {
359 return show(pt, false, -1);
362 static public void showAndResetView(final ProjectThing pt) {
363 launchers.submit(new Runnable() {
364 public void run() {
365 // wait until done
366 Future<Vector<Future<Content>>> fu = show(pt, true, -1);
367 Vector<Future<Content>> vc;
368 try {
369 vc = fu.get(); // wait until done
370 } catch (Exception e) {
371 IJError.print(e);
372 return;
374 for (Future<Content> fc : vc) {
375 try {
376 Content c = fc.get();
377 if (null == c) continue;
378 ArrayList<Display3D> d3ds = new ArrayList<Display3D>();
379 synchronized (ht_layer_sets) {
380 d3ds.addAll(ht_layer_sets.values());
382 /* // Disabled, it's annoying
383 for (Display3D d3d : d3ds) {
384 synchronized (d3d) {
385 if (d3d.universe.getContents().contains(c)) {
386 d3d.universe.resetView(); // reset the absolute center
387 d3d.universe.adjustView(); // zoom out to bring all elements in universe within view
391 } catch (Exception e) {
392 IJError.print(e);
395 Utils.logAll("Reset 3D view if not within field of view!");
400 /** Scan the ProjectThing children and assign the renderable ones to an existing Display3D for their LayerSet, or open a new one. If true == wait && -1 != resample, then the method returns only when the mesh/es have been added. */
401 static public Future<Vector<Future<Content>>> show(final ProjectThing pt, final boolean wait, final int resample) {
402 if (null == pt) return null;
404 Future<Vector<Future<Content>>> fu = launchers.submit(new Callable<Vector<Future<Content>>>() {
405 public Vector<Future<Content>> call() {
407 // Scan the given ProjectThing for 3D-viewable items
408 // So: find arealist, pipe, ball, and profile_list types
409 final HashSet<ProjectThing> hs = pt.findBasicTypeChildren();
410 if (null == hs || 0 == hs.size()) {
411 Utils.logAll("Node " + pt + " does not contain any 3D-displayable children");
412 return null;
415 // Remove profile if it lives under a profile_list
416 for (Iterator<ProjectThing> it = hs.iterator(); it.hasNext(); ) {
417 ProjectThing pt = it.next();
418 if (null != pt.getObject() && pt.getObject().getClass() == Profile.class && pt.getParent().getType().equals("profile_list")) {
419 it.remove();
423 setWaitingCursor();
425 // Start new scheduler to publish/add meshes to the 3D Viewer every 5 seconds and when done.
426 final Hashtable<Display3D,Vector<Content>> contents = new Hashtable<Display3D,Vector<Content>>();
427 final ScheduledExecutorService updater = Executors.newScheduledThreadPool(1);
428 final AtomicInteger counter = new AtomicInteger();
429 updater.scheduleWithFixedDelay(new Runnable() {
430 public void run() {
431 // Obtain a copy of the contents queue
432 HashMap<Display3D,Vector<Content>> m = new HashMap<Display3D,Vector<Content>>();
433 synchronized (contents) {
434 m.putAll(contents);
435 contents.clear();
437 if (m.isEmpty()) return;
438 // Add all to the corresponding Display3D
439 for (Map.Entry<Display3D,Vector<Content>> e : m.entrySet()) {
440 e.getKey().universe.addContentLater(e.getValue());
441 counter.getAndAdd(e.getValue().size());
443 Utils.showStatus(new StringBuilder("Rendered ").append(counter.get()).append('/').append(hs.size()).toString());
445 }, 100, 4000, TimeUnit.MILLISECONDS);
447 // A list of all generated Content objects
448 final Vector<Future<Content>> list = new Vector<Future<Content>>();
450 for (final Iterator<ProjectThing> it = hs.iterator(); it.hasNext(); ) {
451 // obtain the Displayable object under the node
452 final ProjectThing child = it.next();
454 Object obc = child.getObject();
455 final Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
456 if (null != displ) {
457 if (displ.getClass().equals(Profile.class)) {
458 //Utils.log("Display3D can't handle Bezier profiles at the moment.");
459 // handled by profile_list Thing
460 continue;
462 if (!displ.isVisible()) {
463 Utils.log("Skipping non-visible node " + displ);
464 continue;
467 // obtain the containing LayerSet
468 final Display3D d3d;
469 if (null != displ) d3d = Display3D.get(displ.getLayerSet());
470 else if (child.getType().equals("profile_list")) {
471 ArrayList<ProjectThing> al_children = child.getChildren();
472 if (null == al_children || 0 == al_children.size()) continue;
473 // else, get the first Profile and get its LayerSet
474 d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
475 } else {
476 Utils.log("Don't know what to do with node " + child);
477 d3d = null;
479 if (null == d3d) {
480 Utils.log("Could not get a proper 3D display for node " + displ);
481 return null; // java3D not installed most likely
484 boolean already;
485 synchronized (d3d.ht_pt_meshes) {
486 already = d3d.ht_pt_meshes.containsKey(child);
488 if (already) {
489 if (child.getObject() instanceof ZDisplayable) {
490 Utils.log("Updating 3D view of " + child.getObject());
491 } else {
492 Utils.log("Updating 3D view of " + child);
496 list.add(d3d.executors.submit(new Callable<Content>() {
497 public Content call() {
498 Content c = null;
499 try {
500 c = d3d.createMesh(child, displ, resample).call();
501 Vector<Content> vc;
502 synchronized (contents) {
503 vc = contents.get(d3d);
504 if (null == vc) vc = new Vector<Content>();
505 contents.put(d3d, vc);
507 vc.add(c);
508 } catch (Exception e) {
509 IJError.print(e);
511 return c;
513 }));
515 // If it's the last one:
516 if (!it.hasNext()) {
517 // Add the concluding task, that waits on all and shuts down the scheduler
518 d3d.executors.submit(new Runnable() {
519 public void run() {
520 // Wait until all are done
521 for (Future<Content> c : list) {
522 try {
523 c.get();
524 } catch (Throwable t) {
525 IJError.print(t);
528 try {
529 // Shutdown scheduler and execute remaining tasks
530 for (Runnable r : updater.shutdownNow()) {
531 r.run();
533 } catch (Throwable e) {
534 IJError.print(e);
536 // Reset cursor
537 doneWaiting();
538 Utils.showStatus(new StringBuilder("Done rendering ").append(counter.get()).append('/').append(hs.size()).toString());
544 return list;
546 }});
548 if (wait && -1 != resample) {
549 try {
550 fu.get();
551 } catch (Throwable t) {
552 IJError.print(t);
556 return fu;
559 static public void resetView(final LayerSet ls) {
560 Display3D d3d = ht_layer_sets.get(ls);
561 if (null != d3d) d3d.universe.resetView();
564 static public void showOrthoslices(Patch p) {
565 Display3D d3d = get(p.getLayerSet());
566 d3d.adjustResampling();
567 //d3d.universe.resetView();
568 String title = makeTitle(p) + " orthoslices";
569 // remove if present
570 d3d.universe.removeContent(title);
571 PatchStack ps = p.makePatchStack();
572 ImagePlus imp = get8BitStack(ps);
573 Content ct = d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
574 setTransform(ct, ps.getPatch(0));
575 ct.setLocked(true); // locks the added content
578 static public void showVolume(Patch p) {
579 Display3D d3d = get(p.getLayerSet());
580 d3d.adjustResampling();
581 //d3d.universe.resetView();
582 String title = makeTitle(p) + " volume";
583 // remove if present
584 d3d.universe.removeContent(title);
585 PatchStack ps = p.makePatchStack();
586 ImagePlus imp = get8BitStack(ps);
587 Content ct = d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
588 setTransform(ct, ps.getPatch(0));
589 ct.setLocked(true); // locks the added content
592 static private void setTransform(Content ct, Patch p) {
593 final double[] a = new double[6];
594 p.getAffineTransform().getMatrix(a);
595 Calibration cal = p.getLayerSet().getCalibration();
596 // a is: m00 m10 m01 m11 m02 m12
597 // d expects: m01 m02 m03 m04, m11 m12 ...
598 ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
599 a[1], a[3], 0, a[5] * cal.pixelWidth,
600 0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
601 0, 0, 0, 1}));
604 static public void showOrthoslices(final ImagePlus imp, final String title, final int wx, final int wy, final float scale2D, final Layer first) {
605 Display3D d3d = get(first.getParent());
606 d3d.universe.removeContent(title);
607 Content ct = d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, 1);
608 Calibration cal = imp.getCalibration();
609 Transform3D t = new Transform3D(new double[]{1, 0, 0, wx * cal.pixelWidth * scale2D,
610 0, 1, 0, wy * cal.pixelHeight * scale2D,
611 0, 0, scale2D, first.getZ() * cal.pixelWidth * scale2D, // not pixelDepth!
612 0, 0, 0, 1});
613 // why scale2D has to be there at all reflects a horrible underlying setting of the calibration, plus of the scaling in the Display3D.
614 Utils.log(t);
615 ct.applyTransform(t);
616 ct.setLocked(true);
619 /** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
620 * If the PatchStack is already of the right type, it is returned,
621 * otherwise a copy is made in the proper type.
623 static private ImagePlus get8BitStack(final PatchStack ps) {
624 switch (ps.getType()) {
625 case ImagePlus.COLOR_RGB:
626 // convert stack to 8-bit color
627 return ps.createColor256Copy();
628 case ImagePlus.GRAY16:
629 case ImagePlus.GRAY32:
630 // convert stack to 8-bit
631 return ps.createGray8Copy();
632 case ImagePlus.GRAY8:
633 case ImagePlus.COLOR_256:
634 return ps;
635 default:
636 Utils.logAll("Cannot handle stacks of type: " + ps.getType());
637 return null;
641 /** Considers there is only one Display3D for each LayerSet. */
642 static public void remove(ProjectThing pt) {
643 if (null == pt) return;
644 if (null == pt.getObject()) return;
645 Object ob = pt.getObject();
646 if (!(ob instanceof Displayable)) return;
647 Displayable displ = (Displayable)ob;
648 Display3D d3d = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
649 if (null == d3d) {
650 // there is no Display3D showing the pt to remove
651 //Utils.log2("No Display3D contains ProjectThing: " + pt);
652 return;
654 String name;
655 synchronized (d3d.ht_pt_meshes) {
656 name = d3d.ht_pt_meshes.remove(pt);
658 if (null == name) {
659 Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
660 return;
662 d3d.universe.removeContent(name);
666 /** Creates a mesh for the given Displayable in a separate Thread, and adds it to the universe. */
667 private Future<Content> addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
668 return executors.submit(new Callable<Content>() {
669 public Content call() {
670 try {
671 // 1 - Create content
672 Callable<Content> c = createMesh(pt, displ, resample);
673 if (null == c) return null;
674 Content content = c.call();
675 if (null == content) return null;
676 String title = content.getName();
677 // 2 - Remove from universe any content of the same title
678 if (universe.contains(title)) {
679 universe.removeContent(title);
681 // 3 - Add to universe, and wait
682 universe.addContentLater(content).get();
684 return content;
686 } catch (Exception e) {
687 IJError.print(e);
688 return null;
694 static private final String makeProfileListTitle(final ProjectThing pt) {
695 String title;
696 Object ob = pt.getParent().getTitle();
697 if (null == ob || ob.equals(pt.getParent().getType())) title = pt.toString() + " #" + pt.getId(); // Project.getMeaningfulTitle can't handle profile_list properly
698 else title = ob.toString() + " /[" + pt.getParent().getType() + "]/[profile_list] #" + pt.getId();
699 return title;
702 /** Remove all basic type children contained in {@param pt} and its children, recursively.
704 * @param pt
706 static public void removeFrom3D(final ProjectThing pt) {
707 final HashSet<ProjectThing> hs = pt.findBasicTypeChildren();
708 if (null == hs || 0 == hs.size()) {
709 Utils.logAll("Nothing to remove from 3D.");
710 return;
712 // Ignore Profile instances ("profile_list" takes care of them)
713 for (final ProjectThing child : hs) {
714 if (child.getByType().equals("profile")) continue;
715 // Find the LayerSet
716 LayerSet lset = null;
717 if (child.getType().equals("profile_list")) {
718 if (!child.hasChildren()) continue;
719 for (ProjectThing p : child.getChildren()) {
720 if (null != p.getObject() && p.getObject() instanceof Profile) {
721 lset = ((Displayable)p.getObject()).getLayerSet();
722 break;
725 if (null == lset) continue;
726 } else if (child.getType().equals("profile")) {
727 // Taken care of by "profile list"
728 continue;
729 } else {
730 final Displayable d = (Displayable)child.getObject();
731 if (null == d) {
732 Utils.log("Null object for ProjectThing " + child);
733 continue;
735 lset = d.getLayerSet();
737 if (null == lset) {
738 Utils.log("No LayerSet found for " + child);
739 continue;
741 Display3D d3d = getDisplay(lset);
742 if (null == d3d) {
743 Utils.log("No Display 3D found for " + child);
744 continue; // no Display3D window open
746 String oldTitle = d3d.ht_pt_meshes.remove(child);
747 if (null == oldTitle) {
748 Utils.log("Could not find a title for " + child);
749 continue;
751 Utils.log("Removed from 3D view: " + oldTitle);
752 d3d.getUniverse().removeContent(oldTitle);
757 /** Returns a function that returns a Content object.
758 * Does NOT add the Content to the universe; it merely creates it. */
759 public Callable<Content> createMesh(final ProjectThing pt, final Displayable displ, final int resample) {
760 final double scale = 1.0; // OBSOLETE
761 return new Callable<Content>() {
762 public Content call() {
763 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
764 try {
766 // 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.)
767 final List<Point3f> triangles;
768 //boolean no_culling_ = false;
770 final Class<?> c;
771 final boolean line_mesh;
772 final int line_mesh_mode;
773 if (null == displ) {
774 c = null;
775 line_mesh = false;
776 line_mesh_mode = Integer.MAX_VALUE;
777 } else {
778 c = displ.getClass();
779 line_mesh = Tree.class.isAssignableFrom(c) || Polyline.class == c;
780 if (Tree.class.isAssignableFrom(c)) line_mesh_mode = CustomLineMesh.PAIRWISE;
781 else if (Polyline.class == c) line_mesh_mode = CustomLineMesh.CONTINUOUS;
782 else line_mesh_mode = Integer.MAX_VALUE; // disabled
785 List<Point3f> extra_triangles = null;
786 List<Color3f> triangle_colors = null,
787 extra_triangle_colors = null;
789 int rs = resample;
790 if (displ instanceof AreaContainer) {
791 if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
792 else rs = Display3D.this.resample;
794 if (AreaList.class == c) {
795 triangles = ((AreaList)displ).generateTriangles(scale, rs);
796 //triangles = removeNonManifold(triangles);
797 } else if (Ball.class == c) {
798 double[][][] globe = Ball.generateGlobe(12, 12);
799 triangles = ((Ball)displ).generateTriangles(scale, globe);
800 } else if (displ instanceof Line3D) {
801 // Pipe and Polyline
802 // adjustResampling(); // fails horribly, needs first to correct mesh-generation code
803 triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
804 } else if (displ instanceof Tree<?>) {
805 // A 3D wire skeleton, using CustomLineMesh
806 final Tree.MeshData skeleton = ((Tree<?>)displ).generateSkeleton(scale, 12, 1);
807 triangles = skeleton.verts;
808 triangle_colors = skeleton.colors;
809 if (displ instanceof Treeline) {
810 final Tree.MeshData tube = ((Treeline)displ).generateMesh(scale, 12);
811 extra_triangles = tube.verts;
812 extra_triangle_colors = tube.colors;
813 } else if (displ instanceof AreaTree) {
814 final Tree.MeshData mesh = ((AreaTree)displ).generateMesh(scale, rs);
815 extra_triangles = mesh.verts;
816 extra_triangle_colors = mesh.colors;
818 if (null != extra_triangles && extra_triangles.isEmpty()) extra_triangles = null; // avoid issues with MultiMesh
819 } else if (Connector.class == c) {
820 final Tree.MeshData octopus = ((Connector)displ).generateMesh(scale, 12);
821 triangles = octopus.verts;
822 triangle_colors = octopus.colors;
823 } else if (null == displ && pt.getType().equals("profile_list")) {
824 triangles = Profile.generateTriangles(pt, scale);
825 //no_culling_ = true;
826 } else {
827 Utils.log("Unrecognized type for 3D mesh generation: " + (null != displ ? displ.getClass() : null) + " : " + displ);
828 triangles = null;
830 // safety checks
831 if (null == triangles) {
832 Utils.log("Some error ocurred: can't create triangles for " + displ);
833 return null;
835 if (0 == triangles.size()) {
836 Utils.log2("Skipping empty mesh for " + displ.getTitle());
837 return null;
839 if (!line_mesh && 0 != triangles.size() % 3) {
840 Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
841 return null;
844 final Color color;
845 final float alpha;
846 final String title;
847 if (null != displ) {
848 color = displ.getColor();
849 alpha = displ.getAlpha();
850 title = makeTitle(displ);
851 } else if (pt.getType().equals("profile_list")) {
852 // for profile_list: get from the first (what a kludge; there should be a ZDisplayable ProfileList object)
853 Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
854 if (null == obp) return null;
855 Displayable di = (Displayable)obp;
856 color = di.getColor();
857 alpha = di.getAlpha();
858 title = makeProfileListTitle(pt);
859 } else {
860 title = pt.toString() + " #" + pt.getId();
861 color = null;
862 alpha = 1.0f;
865 // 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.
866 final boolean no_culling = true; // for ALL
868 Content ct = null;
870 try {
871 Color3f c3 = new Color3f(color);
873 // If it exists, remove and add as new:
874 universe.removeContent(title);
876 final CustomMesh cm;
878 if (line_mesh) {
879 //ct = universe.createContent(new CustomLineMesh(triangles, line_mesh_mode, c3, 0), title);
880 cm = new CustomLineMesh(triangles, line_mesh_mode, c3, 0);
881 } else if (no_culling) {
882 // create a mesh with the same color and zero transparency (that is, full opacity)
883 CustomTriangleMesh mesh = new CustomTriangleMesh(triangles, c3, 0);
884 // Set mesh properties for double-sided triangles
885 PolygonAttributes pa = mesh.getAppearance().getPolygonAttributes();
886 pa.setCullFace(PolygonAttributes.CULL_NONE);
887 pa.setBackFaceNormalFlip(true);
888 mesh.setColor(c3);
889 // After setting properties, add to the viewer
890 //ct = universe.createContent(mesh, title);
891 cm = mesh;
892 } else {
893 //ct = universe.createContent(new CustomTriangleMesh(triangles, c3, 0), title);
894 cm = new CustomTriangleMesh(triangles, c3, 0);
897 if (null != triangle_colors) cm.setColor(triangle_colors);
899 //if (null == cm) return null;
901 if (null == extra_triangles || 0 == extra_triangles.size()) {
902 ct = universe.createContent(cm, title);
903 } else {
904 final CustomTriangleMesh extra = new CustomTriangleMesh(extra_triangles, c3, 0);
905 if (null != extra_triangle_colors) {
906 // Set mesh properties for double-sided triangles
907 PolygonAttributes pa = extra.getAppearance().getPolygonAttributes();
908 pa.setCullFace(PolygonAttributes.CULL_NONE);
909 pa.setBackFaceNormalFlip(true);
910 extra.setColor(extra_triangle_colors);
912 ct = universe.createContent(new CustomMultiMesh(Arrays.asList(new CustomMesh[]{cm, extra})), title);
915 // Set general content properties
916 ct.setTransparency(1f - alpha);
917 // Default is unlocked (editable) transformation; set it to locked:
918 ct.setLocked(true);
920 // register mesh title
921 synchronized (ht_pt_meshes) {
922 ht_pt_meshes.put(pt, ct.getName());
925 } catch (Throwable e) {
926 Utils.logAll("Mesh generation failed for \"" + title + "\" from " + pt);
927 IJError.print(e);
928 e.printStackTrace();
931 Utils.log2(pt.toString() + " n points: " + triangles.size());
933 return ct;
935 } catch (Exception e) {
936 IJError.print(e);
937 return null;
943 static public class VectorStringContent {
944 VectorString3D vs;
945 String title;
946 Color color;
947 double[] widths;
948 float alpha;
949 public VectorStringContent(VectorString3D vs, String title, Color color, double[] widths, float alpha){
950 this.vs = vs;
951 this.title = title;
952 this.color = color;
953 this.widths = widths;
954 this.alpha = alpha;
956 public Content asContent(Display3D d3d) {
957 double[] wi = widths;
958 if (null == widths) {
959 wi = new double[vs.getPoints(0).length];
960 Arrays.fill(wi, 2.0);
961 } else if (widths.length != vs.length()) {
962 Utils.log("ERROR: widths.length != VectorString3D.length()");
963 return null;
965 float transp = 1 - alpha;
966 if (transp < 0) transp = 0;
967 if (transp > 1) transp = 1;
968 if (1 == transp) {
969 Utils.log("WARNING: adding a 3D object fully transparent.");
971 List<Point3f> triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), d3d.scale);
972 Content ct = d3d.universe.createContent(new CustomTriangleMesh(triangles, new Color3f(color), 0), title);
973 ct.setTransparency(transp);
974 ct.setLocked(true);
975 return ct;
979 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
980 static public Future<Collection<Future<Content>>> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
981 return addMesh(ref_ls, vs, title, color, null, 1.0f);
984 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
985 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) {
986 List<Content> col = new ArrayList<Content>();
987 Display3D d3d = Display3D.get(ref_ls);
988 col.add(new VectorStringContent(vs, title, color, widths, alpha).asContent(d3d));
989 return d3d.addContent(col);
992 static public Future<Collection<Future<Content>>> show(final LayerSet ref_ls, final Collection<Content> col) {
993 Display3D d3d = get(ref_ls);
994 return d3d.addContent(col);
997 public Future<Collection<Future<Content>>> addContent(final Collection<Content> col) {
999 final FutureTask<Collection<Future<Content>>> fu = new FutureTask<Collection<Future<Content>>>(new Callable<Collection<Future<Content>>>() {
1000 public Collection<Future<Content>> call() {
1001 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
1002 try {
1003 return universe.addContentLater(col);
1004 } catch (Throwable e) {
1005 IJError.print(e);
1006 return null;
1008 }});
1010 launchers.submit(new Runnable() { public void run() {
1011 executors.submit(fu);
1012 }});
1014 return fu;
1017 public Future<Content> addContent(final Content c) {
1018 final FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
1019 public Content call() {
1020 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
1021 try {
1022 return universe.addContentLater(c).get();
1023 } catch (Throwable e) {
1024 IJError.print(e);
1025 return null;
1030 launchers.submit(new Runnable() { public void run() {
1031 executors.submit(fu);
1032 }});
1034 return fu;
1037 static public final int estimateResamplingFactor(final LayerSet ls, final double width, final double height) {
1038 final int max_dimension = ls.getPixelsMaxDimension();
1039 return (int)(DEFAULT_RESAMPLE / (Math.max(width, height) > max_dimension ?
1040 max_dimension / Math.max(width, height)
1041 : 1));
1044 /** Estimate a scaling factor, to be used as a multiplier of the suggested default resampling. */
1045 private final int estimateResamplingFactor() {
1046 return estimateResamplingFactor(layer_set, width, height);
1049 // This method has the exclusivity in adjusting the resampling value, and it also returns it.
1050 synchronized private final int adjustResampling() {
1051 if (resample > 0) return resample;
1052 final GenericDialog gd = new GenericDialog("Resample");
1053 final int default_resample = estimateResamplingFactor();
1054 gd.addSlider("Resample: ", 1, Math.max(default_resample, 100), -1 != resample ? resample : default_resample);
1055 gd.showDialog();
1056 if (gd.wasCanceled()) {
1057 resample = -1 != resample ? resample : default_resample; // current or default value
1058 return resample;
1060 resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
1061 return resample;
1064 /** Checks if there is any Display3D instance currently showing the given Displayable. */
1065 static public boolean isDisplayed(final Displayable d) {
1066 if (null == d) return false;
1067 final String title = makeTitle(d);
1068 for (Display3D d3d : ht_layer_sets.values()) {
1069 if (null != d3d.universe.getContent(title)) return true;
1071 if (d.getClass() == Profile.class) {
1072 if (null != getProfileContent(d)) return true;
1074 return false;
1077 /** 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. */
1078 static public Content getProfileContent(final Displayable d) {
1079 if (null == d) return null;
1080 if (d.getClass() != Profile.class) return null;
1081 Display3D d3d = get(d.getLayer().getParent());
1082 if (null == d3d) return null;
1083 ProjectThing pt = d.getProject().findProjectThing(d);
1084 if (null == pt) return null;
1085 pt = (ProjectThing) pt.getParent();
1086 return d3d.universe.getContent(new StringBuilder(pt.toString()).append(" #").append(pt.getId()).toString());
1089 static public Future<Boolean> setColor(final Displayable d, final Color color) {
1090 final Display3D d3d = getDisplay(d.getLayer().getParent());
1091 if (null == d3d) return null; // no 3D displays open
1092 return d3d.executors.submit(new Callable<Boolean>() { public Boolean call() {
1093 Content content = d3d.universe.getContent(makeTitle(d));
1094 if (null == content) content = getProfileContent(d);
1095 if (null != content) {
1096 content.setColor(new Color3f(color));
1097 return true;
1099 return false;
1100 }});
1103 static public Future<Boolean> setTransparency(final Displayable d, final float alpha) {
1104 if (null == d) return null;
1105 Layer layer = d.getLayer();
1106 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
1107 final Display3D d3d = ht_layer_sets.get(layer.getParent());
1108 if (null == d3d) return null;
1109 return d3d.executors.submit(new Callable<Boolean>() { public Boolean call() {
1110 String title = makeTitle(d);
1111 Content content = d3d.universe.getContent(title);
1112 if (null == content) content = getProfileContent(d);
1113 if (null != content) content.setTransparency(1 - alpha);
1114 else if (null == content && d.getClass().equals(Patch.class)) {
1115 Patch pa = (Patch)d;
1116 if (pa.isStack()) {
1117 title = pa.getProject().getLoader().getFileName(pa);
1118 for (Display3D dd : ht_layer_sets.values()) {
1119 for (Iterator<?> cit = dd.universe.getContents().iterator(); cit.hasNext(); ) {
1120 Content c = (Content)cit.next();
1121 if (c.getName().startsWith(title)) {
1122 c.setTransparency(1 - alpha);
1123 // no break, since there could be a volume and an orthoslice
1129 return true;
1130 }});
1133 static public String makeTitle(final Displayable d) {
1134 return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
1136 static public String makeTitle(final Patch p) {
1137 return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
1138 + " #" + p.getProject().getLoader().getNextId();
1141 /** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
1142 * (otherwise returns null). */
1143 static public Future<Content> update(final Displayable d) {
1144 Layer layer = d.getLayer();
1145 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
1146 Display3D d3d = ht_layer_sets.get(layer.getParent());
1147 if (null == d3d) return null;
1148 return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
1152 static public final double computeTriangleArea() {
1153 return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
1154 Math.pow(yA*zB + yB*zC + yC*zA, 2) +
1155 Math.pow(zA*xB + zB*xC + zC*xA, 2));
1159 static public final boolean contains(final LayerSet ls, final String title) {
1160 final Display3D d3d = getDisplay(ls);
1161 if (null == d3d) return false;
1162 return null != d3d.universe.getContent(title);
1165 static public void destroy() {
1166 launchers.shutdownNow();
1169 static public void init() {
1170 if (launchers.isShutdown()) {
1171 launchers = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-launchers");
1175 /** Creates a calibrated sphere to represent a point at LayerSet pixel coordinates wx, wy, wz, with radius wr.*/
1176 public List<Point3f> createFatPoint(final double wx, final double wy, final double wz, final double wr, final Calibration cal) {
1177 final double[][][] globe = Ball.generateGlobe(12, 12);
1178 final int sign = cal.pixelDepth < 0 ? -1 : 1;
1179 for (int z=0; z<globe.length; z++) {
1180 for (int k=0; k<globe[0].length; k++) {
1181 globe[z][k][0] = (globe[z][k][0] * wr + wx) * scale * cal.pixelWidth;
1182 globe[z][k][1] = (globe[z][k][1] * wr + wy) * scale * cal.pixelHeight;
1183 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.
1186 final ArrayList<Point3f> list = new ArrayList<Point3f>();
1187 // create triangular faces and add them to the list
1188 for (int z=0; z<globe.length-1; z++) { // the parallels
1189 for (int k=0; k<globe[0].length -1; k++) { // meridian points
1190 // half quadrant (a triangle)
1191 list.add(new Point3f((float)globe[z][k][0], (float)globe[z][k][1], (float)globe[z][k][2]));
1192 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]));
1193 list.add(new Point3f((float)globe[z+1][k][0], (float)globe[z+1][k][1], (float)globe[z+1][k][2]));
1194 // the other half quadrant
1195 list.add(new Point3f((float)globe[z][k][0], (float)globe[z][k][1], (float)globe[z][k][2]));
1196 list.add(new Point3f((float)globe[z][k+1][0], (float)globe[z][k+1][1], (float)globe[z][k+1][2]));
1197 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]));
1200 return list;
1203 /** Expects uncalibrated wx,wy,wz, (i.e. pixel values), to be calibrated by @param ls calibration. */
1204 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) {
1205 Display3D d3d = Display3D.get(ls);
1206 d3d.universe.removeContent(title);
1207 Content ct = d3d.universe.createContent(new CustomTriangleMesh(d3d.createFatPoint(wx, wy, wz, wr, ls.getCalibrationCopy()), new Color3f(color), 0), title);
1208 ct.setLocked(true);
1209 return d3d.addContent(ct);