Updated copyright dates.
[trakem2.git] / ini / trakem2 / display / Display3D.java
blob25cfe992278f7ad19f89d3e018e997789bf8dcb4
1 package ini.trakem2.display;
3 import ini.trakem2.tree.*;
4 import ini.trakem2.utils.*;
5 import ini.trakem2.imaging.PatchStack;
6 import ini.trakem2.vector.VectorString3D;
8 import ij.ImageStack;
9 import ij.ImagePlus;
10 import ij.process.ImageProcessor;
11 import ij.process.ByteProcessor;
12 import ij.gui.ShapeRoi;
13 import ij.gui.GenericDialog;
14 import ij.io.DirectoryChooser;
15 import ij.measure.Calibration;
17 import java.awt.Color;
18 import java.awt.Cursor;
19 import java.awt.Rectangle;
20 import java.awt.geom.Area;
21 import java.awt.MenuBar;
22 import java.awt.Menu;
23 import java.awt.MenuItem;
24 import java.awt.CheckboxMenuItem;
25 import java.awt.Component;
26 import java.awt.event.ActionListener;
27 import java.awt.event.ItemListener;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ItemEvent;
30 import java.util.*;
31 import java.io.File;
32 import java.awt.geom.AffineTransform;
34 import java.awt.event.WindowAdapter;
35 import java.awt.event.WindowEvent;
36 import java.awt.event.KeyEvent;
37 import java.awt.event.KeyAdapter;
38 import java.awt.event.KeyListener;
40 import javax.vecmath.Point3f;
41 import javax.vecmath.Color3f;
42 import javax.vecmath.Vector3f;
43 import javax.media.j3d.View;
44 import javax.media.j3d.Transform3D;
45 import javax.media.j3d.PolygonAttributes;
47 import ij3d.ImageWindow3D;
48 import ij3d.Image3DUniverse;
49 import ij3d.Content;
50 import ij3d.Image3DMenubar;
51 import customnode.CustomMeshNode;
52 import customnode.CustomMesh;
53 import customnode.CustomTriangleMesh;
55 import java.lang.reflect.Field;
56 import java.util.concurrent.ExecutorService;
57 import java.util.concurrent.Executors;
58 import java.util.concurrent.Future;
59 import java.util.concurrent.TimeUnit;
60 import java.util.concurrent.FutureTask;
61 import java.util.concurrent.Callable;
64 /** One Display3D instance for each LayerSet (maximum). */
65 public final class Display3D {
67 /** Table of LayerSet and Display3D - since there is a one to one relationship. */
68 static private Hashtable ht_layer_sets = new Hashtable();
69 /**Control calls to new Display3D. */
70 static private Lock htlock = new Lock();
72 /** 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. */
73 static public Hashtable getMasterTable() {
74 return (Hashtable)ht_layer_sets.clone();
77 /** Table of ProjectThing keys versus meshes, the latter represented by List of triangles in the form of thre econsecutive Point3f in the List.*/
78 private Hashtable ht_pt_meshes = new Hashtable();
80 private Image3DUniverse universe;
82 private Lock u_lock = new Lock();
84 private LayerSet layer_set;
85 private double width, height;
86 private int resample = -1; // unset
87 static private final int DEFAULT_RESAMPLE = 4;
88 /** If the LayerSet dimensions are too large, then limit to max 2048 for width or height and setup a scale.*/
89 private double scale = 1.0;
90 static private final int MAX_DIMENSION = 1024;
92 private String selected = null;
94 // To fork away from the EventDispatchThread
95 static private ExecutorService launchers = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
97 // To build meshes
98 private ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
101 static private KeyAdapter ka = new KeyAdapter() {
102 public void keyPressed(KeyEvent ke) {
103 // F1 .. F12 keys to set tools
104 ProjectToolbar.keyPressed(ke);
109 /** Defaults to parallel projection. */
110 private Display3D(final LayerSet ls) {
111 this.layer_set = ls;
112 this.universe = new Image3DUniverse(512, 512); // size of the initial canvas, not the universe itself
113 this.universe.getViewer().getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); // (View.PERSPECTIVE_PROJECTION);
114 computeScale(ls);
115 this.universe.show();
116 this.universe.getWindow().addWindowListener(new IW3DListener(this, ls));
117 // it ignores the listeners:
118 //preaddKeyListener(this.universe.getWindow(), ka);
119 //preaddKeyListener(this.universe.getWindow().getCanvas(), ka);
121 // register
122 Display3D.ht_layer_sets.put(ls, this);
126 private void preaddKeyListener(Component c, KeyListener kl) {
127 KeyListener[] all = c.getKeyListeners();
128 if (null != all) {
129 for (KeyListener k : all) c.removeKeyListener(k);
131 c.addKeyListener(kl);
132 if (null != all) {
133 for (KeyListener k : all) c.addKeyListener(k);
138 public Image3DUniverse getUniverse() {
139 return universe;
142 /* Take a snapshot know-it-all mode. Each Transform3D given as argument gets assigned to the (nearly) homonimous TransformGroup, which have the following relationships:
144 * scaleTG contains rotationsTG
145 * rotationsTG contains translateTG
146 * translateTG contains centerTG
147 * centerTG contains the whole scene, with all meshes, etc.
149 * Any null arguments imply the current transform in the open Display3D.
151 * 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.
153 * The TransformGroup instances may be reached like this:
155 * LayerSet layer_set = Display.getFrontLayer().getParent();
156 * Display3D d3d = Display3D.getDisplay(layer_set);
157 * TransformGroup scaleTG = d3d.getUniverse().getGlobalScale();
158 * TransformGroup rotationsTG = d3d.getUniverse().getGlobalRotate();
159 * TransformGroup translateTG = d3d.getUniverse().getGlobalTranslate();
160 * TransformGroup centerTG = d3d.getUniverse().getCenterTG();
162 * ... and the Transform3D from each may be read out indirectly like this:
164 * Transform3D t_scale = new Transform3D();
165 * scaleTG.getTransform(t_scale);
166 * ...
168 * 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).
171 /*public ImagePlus makeSnapshot(final Transform3D scale, final Transform3D rotate, final Transform3D translate, final Transform3D center) {
172 return universe.makeSnapshot(scale, rotate, translate, center);
175 /** Uses current scaling, translation and centering transforms! */
176 /*public ImagePlus makeSnapshotXY() { // aka posterior
177 // default view
178 return universe.makeSnapshot(null, new Transform3D(), null, null);
180 /** Uses current scaling, translation and centering transforms! */
181 /*public ImagePlus makeSnapshotXZ() { // aka dorsal
182 Transform3D rot1 = new Transform3D();
183 rot1.rotZ(-Math.PI/2);
184 Transform3D rot2 = new Transform3D();
185 rot2.rotX(Math.PI/2);
186 rot1.mul(rot2);
187 return universe.makeSnapshot(null, rot1, null, null);
190 /** Uses current scaling, translation and centering transforms! */
192 public ImagePlus makeSnapshotYZ() { // aka lateral
193 Transform3D rot = new Transform3D();
194 rot.rotY(Math.PI/2);
195 return universe.makeSnapshot(null, rot, null, null);
199 public ImagePlus makeSnapshotZX() { // aka frontal
200 Transform3D rot = new Transform3D();
201 rot.rotX(-Math.PI/2);
202 return universe.makeSnapshot(null, rot, null, null);
206 /** Uses current scaling, translation and centering transforms! Opposite side of XZ. */
208 public ImagePlus makeSnapshotXZOpp() {
209 Transform3D rot1 = new Transform3D();
210 rot1.rotX(-Math.PI/2); // 90 degrees clockwise
211 Transform3D rot2 = new Transform3D();
212 rot2.rotY(Math.PI); // 180 degrees around Y, to the other side.
213 rot1.mul(rot2);
214 return universe.makeSnapshot(null, rot1, null, null);
217 private class IW3DListener extends WindowAdapter {
218 private Display3D d3d;
219 private LayerSet ls;
220 IW3DListener(Display3D d3d, LayerSet ls) {
221 this.d3d = d3d;
222 this.ls = ls;
224 public void windowClosing(WindowEvent we) {
225 //Utils.log2("Display3D.windowClosing");
226 d3d.executors.shutdownNow();
227 /*Object ob =*/ ht_layer_sets.remove(ls);
228 /*if (null != ob) {
229 Utils.log2("Removed Display3D from table for LayerSet " + ls);
232 public void windowClosed(WindowEvent we) {
233 //Utils.log2("Display3D.windowClosed");
234 ht_layer_sets.remove(ls);
238 /** Reads the #ID in the name, which is immutable. */
239 private ProjectThing find(String name) {
240 long id = Long.parseLong(name.substring(name.lastIndexOf('#')+1));
241 for (Iterator it = ht_pt_meshes.keySet().iterator(); it.hasNext(); ) {
242 ProjectThing pt = (ProjectThing)it.next();
243 Displayable d = (Displayable)pt.getObject();
244 if (d.getId() == id) {
245 return pt;
248 return null;
251 /** If the layer set is too large in width and height, then set a scale that makes it maximum MAX_DIMENSION in any of the two dimensions. */
252 private void computeScale(LayerSet ls) {
253 this.width = ls.getLayerWidth();
254 this.height = ls.getLayerHeight();
255 if (width > MAX_DIMENSION) {
256 scale = MAX_DIMENSION / width;
257 height *= scale;
258 width = MAX_DIMENSION;
260 if (height > MAX_DIMENSION) {
261 scale = MAX_DIMENSION / height;
262 width *= scale;
263 height = MAX_DIMENSION;
265 //Utils.log2("scale, width, height: " + scale + ", " + width + ", " + height);
268 static private boolean check_j3d = true;
269 static private boolean has_j3d_3dviewer = false;
271 static private boolean hasLibs() {
272 if (check_j3d) {
273 check_j3d = false;
274 try {
275 Class p3f = Class.forName("javax.vecmath.Point3f");
276 has_j3d_3dviewer = true;
277 } catch (ClassNotFoundException cnfe) {
278 Utils.log("Java 3D not installed.");
279 has_j3d_3dviewer = false;
280 return false;
282 try {
283 Class ij3d = Class.forName("ij3d.ImageWindow3D");
284 has_j3d_3dviewer = true;
285 } catch (ClassNotFoundException cnfe) {
286 Utils.log("3D Viewer not installed.");
287 has_j3d_3dviewer = false;
288 return false;
291 return has_j3d_3dviewer;
294 /** Get an existing Display3D for the given LayerSet, or create a new one for it (and cache it). */
295 static private Display3D get(final LayerSet ls) {
296 synchronized (htlock) {
297 htlock.lock();
298 try {
299 // test:
300 if (!hasLibs()) return null;
302 Object ob = ht_layer_sets.get(ls);
303 if (null == ob) {
304 final boolean[] done = new boolean[]{false};
305 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() {
306 Display3D ob = new Display3D(ls);
307 ht_layer_sets.put(ls, ob);
308 done[0] = true;
309 }});
310 // wait to avoid crashes in amd64
311 // try { Thread.sleep(500); } catch (Exception e) {}
312 while (!done[0]) {
313 try { Thread.sleep(50); } catch (Exception e) {}
315 ob = ht_layer_sets.get(ls);
317 return (Display3D)ob;
318 } catch (Exception e) {
319 IJError.print(e);
320 } finally {
321 // executed even when returning from within the try-catch block
322 htlock.unlock();
325 return null;
328 /** Get the Display3D instance that exists for the given LayerSet, if any. */
329 static public Display3D getDisplay(final LayerSet ls) {
330 return (Display3D)ht_layer_sets.get(ls);
333 static public void setWaitingCursor() {
334 for (Iterator it = ht_layer_sets.values().iterator(); it.hasNext(); ) {
335 ((Display3D)it.next()).universe.getWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
339 static public void doneWaiting() {
340 for (Iterator it = ht_layer_sets.values().iterator(); it.hasNext(); ) {
341 ((Display3D)it.next()).universe.getWindow().setCursor(Cursor.getDefaultCursor());
345 static public Future<List<Content>> show(ProjectThing pt) {
346 return show(pt, false, -1);
349 static public void showAndResetView(final ProjectThing pt) {
350 new Thread() { public void run() {
351 setPriority(Thread.NORM_PRIORITY);
352 // wait until done
353 Future<List<Content>> fu = show(pt, true, -1);
354 try {
355 fu.get(); // wait until done
356 } catch (Exception e) { IJError.print(e); }
357 Display3D.resetView(pt.getProject().getRootLayerSet());
358 }}.start();
361 /** 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. */
362 static public Future<List<Content>> show(final ProjectThing pt, final boolean wait, final int resample) {
363 if (null == pt) return null;
364 final Callable<List<Content>> c = new Callable<List<Content>>() {
365 public List<Content> call() {
366 try {
367 // scan the given ProjectThing for 3D-viewable items not present in the ht_meshes
368 // So: find arealist, pipe, ball, and profile_list types
369 final HashSet hs = pt.findBasicTypeChildren();
370 if (null == hs || 0 == hs.size()) {
371 Utils.log("Node " + pt + " contains no 3D-displayable children");
372 return null;
375 final List<Content> list = new ArrayList<Content>();
377 for (final Iterator it = hs.iterator(); it.hasNext(); ) {
378 // obtain the Displayable object under the node
379 final ProjectThing child = (ProjectThing)it.next();
380 Object obc = child.getObject();
381 Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
382 if (null != displ) {
383 if (displ.getClass().equals(Profile.class)) {
384 //Utils.log("Display3D can't handle Bezier profiles at the moment.");
385 // handled by profile_list Thing
386 continue;
388 if (!displ.isVisible()) {
389 Utils.log("Skipping non-visible node " + displ);
390 continue;
393 //StopWatch sw = new StopWatch();
394 // obtain the containing LayerSet
395 Display3D d3d = null;
396 if (null != displ) d3d = Display3D.get(displ.getLayerSet());
397 else if (child.getType().equals("profile_list")) {
398 ArrayList al_children = child.getChildren();
399 if (null == al_children || 0 == al_children.size()) continue;
400 // else, get the first Profile and get its LayerSet
401 d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
402 } else {
403 Utils.log("Don't know what to do with node " + child);
405 if (null == d3d) {
406 Utils.log("Could not get a proper 3D display for node " + displ);
407 return null; // java3D not installed most likely
409 if (d3d.ht_pt_meshes.contains(child)) {
410 Utils.log2("Already here: " + child);
411 continue; // already here
413 setWaitingCursor(); // the above may be creating a display
414 //sw.elapsed("after creating and/or retrieving Display3D");
415 Future<Content> fu = d3d.addMesh(child, displ, resample);
416 if (wait && -1 != d3d.resample) {
417 list.add(fu.get());
420 //sw.elapsed("after creating mesh");
423 return list;
425 } catch (Exception e) {
426 IJError.print(e);
427 return null;
428 } finally {
429 doneWaiting();
433 return launchers.submit(c);
436 static public void resetView(final LayerSet ls) {
437 Display3D d3d = (Display3D) ht_layer_sets.get(ls);
438 if (null != d3d) d3d.universe.resetView();
441 static public void showOrthoslices(Patch p) {
442 Display3D d3d = get(p.getLayerSet());
443 d3d.adjustResampling();
444 //d3d.universe.resetView();
445 String title = makeTitle(p) + " orthoslices";
446 // remove if present
447 d3d.universe.removeContent(title);
448 PatchStack ps = p.makePatchStack();
449 ImagePlus imp = get8BitStack(ps);
450 d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
451 Content ct = d3d.universe.getContent(title);
452 setTransform(ct, ps.getPatch(0));
453 ct.toggleLock(); // locks the added content
456 static public void showVolume(Patch p) {
457 Display3D d3d = get(p.getLayerSet());
458 d3d.adjustResampling();
459 //d3d.universe.resetView();
460 String title = makeTitle(p) + " volume";
461 // remove if present
462 d3d.universe.removeContent(title);
463 PatchStack ps = p.makePatchStack();
464 ImagePlus imp = get8BitStack(ps);
465 d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
466 Content ct = d3d.universe.getContent(title);
467 setTransform(ct, ps.getPatch(0));
468 ct.toggleLock(); // locks the added content
471 static private void setTransform(Content ct, Patch p) {
472 final double[] a = new double[6];
473 p.getAffineTransform().getMatrix(a);
474 Calibration cal = p.getLayerSet().getCalibration();
475 // a is: m00 m10 m01 m11 m02 m12
476 // d expects: m01 m02 m03 m04, m11 m12 ...
477 ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
478 a[1], a[3], 0, a[5] * cal.pixelWidth,
479 0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
480 0, 0, 0, 1}));
483 /** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
484 * If the PatchStach is already of the right type, it is returned,
485 * otherwise a copy is made in the proper type.
487 static private ImagePlus get8BitStack(final PatchStack ps) {
488 switch (ps.getType()) {
489 case ImagePlus.COLOR_RGB:
490 // convert stack to 8-bit color
491 return ps.createColor256Copy();
492 case ImagePlus.GRAY16:
493 case ImagePlus.GRAY32:
494 // convert stack to 8-bit
495 return ps.createGray8Copy();
496 default:
497 return ps;
501 /** A Material, but avoiding name colisions. */
502 static private int mat_index = 1;
503 static private class Mtl {
504 float alpha = 1;
505 float R = 1;
506 float G = 1;
507 float B = 1;
508 String name;
509 Mtl(float alpha, float R, float G, float B) {
510 this.alpha = alpha;
511 this.R = R;
512 this.G = G;
513 this.B = B;
514 name = "mat_" + mat_index;
515 mat_index++;
517 public boolean equals(Object ob) {
518 if (ob instanceof Display3D.Mtl) {
519 Mtl mat = (Mtl)ob;
520 if (mat.alpha == alpha
521 && mat.R == R
522 && mat.G == G
523 && mat.B == B) {
524 return true;
527 return false;
529 void fill(StringBuffer sb) {
530 sb.append("\nnewmtl ").append(name).append('\n')
531 .append("Ns 96.078431\n")
532 .append("Ka 0.0 0.0 0.0\n")
533 .append("Kd ").append(R).append(' ').append(G).append(' ').append(B).append('\n') // this is INCORRECT but I'll figure out the conversion later
534 .append("Ks 0.5 0.5 0.5\n")
535 .append("Ni 1.0\n")
536 .append("d ").append(alpha).append('\n')
537 .append("illum 2\n\n");
539 int getAsSingle() {
540 return (int)((R + G + B) / 3 * 255); // something silly
544 /** Generates DXF file from a table of ProjectThing and their associated triangles. */
545 private String createDXF(Hashtable ht_content) {
546 StringBuffer sb_data = new StringBuffer("0\nSECTION\n2\nENTITIES\n"); //header of file
547 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
548 Map.Entry entry = (Map.Entry)it.next();
549 ProjectThing pt = (ProjectThing)entry.getKey();
550 Displayable displ = (Displayable)pt.getObject();
551 List triangles = (List)entry.getValue();
552 float[] color = displ.getColor().getColorComponents(null);
553 Mtl mtl = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
554 writeTrianglesDXF(sb_data, triangles, mtl.name, Integer.toString(mtl.getAsSingle()));
556 sb_data.append("0\nENDSEC\n0\nEOF\n"); //TRAILER of the file
557 return sb_data.toString();
560 /** @param format works as extension as well. */
561 private void export(final ProjectThing pt, final String format) {
562 if (0 == ht_pt_meshes.size()) return;
563 // select file
564 File file = Utils.chooseFile("untitled", format);
565 if (null == file) return;
566 final String name = file.getName();
567 String name2 = name;
568 if (!name2.endsWith("." + format)) {
569 name2 += "." + format;
571 File f2 = new File(file.getParent() + "/" + name2);
572 int i = 1;
573 while (f2.exists()) {
574 name2 = name + "_" + i + "." + format;
575 f2 = new File(name2);
577 Hashtable ht_content = ht_pt_meshes;
578 if (null != pt) {
579 ht_content = new Hashtable();
580 ht_content.put(pt, ht_pt_meshes.get(pt));
582 if (format.equals("obj")) {
583 String[] data = createObjAndMtl(name2, ht_content);
584 Utils.saveToFile(f2, data[0]);
585 Utils.saveToFile(new File(f2.getParent() + "/" + name2 + ".mtl"), data[1]);
586 } else if (format.equals("dxf")) {
587 Utils.saveToFile(f2, createDXF(ht_content));
591 /** Wavefront format. Returns the contents of two files: one for materials, another for meshes*/
592 private String[] createObjAndMtl(final String file_name, final Hashtable ht_content) {
593 StringBuffer sb_obj = new StringBuffer("# TrakEM2 OBJ File\n");
594 sb_obj.append("mtllib ").append(file_name).append(".mtl").append('\n');
596 Hashtable ht_mat = new Hashtable();
598 int j = 1; // Vert indices in .obj files are global, not reset for every object.
599 // starting at '1' because vert indices start at one.
601 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
602 Map.Entry entry = (Map.Entry)it.next(); // I hate java's gratuituous verbosity
603 ProjectThing pt = (ProjectThing)entry.getKey();
604 Displayable displ = (Displayable)pt.getObject();
605 List triangles = (List)entry.getValue();
606 // make material, and see whether it exists already
607 float[] color = displ.getColor().getColorComponents(null);
608 Mtl mat = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
609 Object mat2 = ht_mat.get(mat);
610 if (null != mat2) mat = (Mtl)mat2; // recycling
611 else ht_mat.put(mat, mat); // !@#$% Can't get the object in a HashSet easily
612 // make list of vertices
613 String title = displ.getProject().getMeaningfulTitle(displ).replaceAll(" ", "_").replaceAll("#", "--");
614 Hashtable ht_points = new Hashtable(); // because we like inefficiency
615 sb_obj.append("o ").append(title).append('\n');
616 final int len = triangles.size();
617 int[] index = new int[len];
618 int k = 0; // iterate over index array, to make faces later
619 // j is tag for each new vert, which start at 1 (for some ridiculous reason)
620 for (Iterator tt = triangles.iterator(); tt.hasNext(); ) {
621 Point3f p = (Point3f)tt.next();
622 //no need if coords are not displaced//p = (Point3f)p.clone();
623 // check if point already exists
624 Object ob = ht_points.get(p);
625 if (null != ob) {
626 index[k] = ((Integer)ob).intValue();
627 } else {
628 // new point
629 index[k] = j;
630 // record
631 ht_points.put(p, new Integer(j));
632 // append vertex
633 sb_obj.append('v').append(' ').append(p.x)
634 .append(' ').append(p.y)
635 .append(' ').append(p.z).append('\n');
636 j++;
638 k++;
640 sb_obj.append("usemtl ").append(mat.name).append('\n');
641 sb_obj.append("s 1\n");
642 if (0 != len % 3) Utils.log2("WARNING: list of triangles not multiple of 3");
643 // print faces
644 int len_p = ht_points.size();
645 for (int i=0; i<len; i+=3) {
646 sb_obj.append('f').append(' ').append(index[i])
647 .append(' ').append(index[i+1])
648 .append(' ').append(index[i+2]).append('\n');
649 //if (index[i] > len_p) Utils.log2("WARNING: face vert index beyond range"); // range is from 1 to len_p inclusive
650 //if (index[i+1] > len_p) Utils.log2("WARNING: face vert index beyond range");
651 //if (index[i+2] > len_p) Utils.log2("WARNING: face vert index beyond range");
652 //Utils.log2("j: " + index[i]);
653 // checks passed
655 sb_obj.append('\n');
657 // make mtl file
658 StringBuffer sb_mtl = new StringBuffer("# TrakEM2 MTL File\n");
659 for (Iterator it = ht_mat.keySet().iterator(); it.hasNext(); ) {
660 Mtl mat = (Mtl)it.next();
661 mat.fill(sb_mtl);
664 return new String[]{sb_obj.toString(), sb_mtl.toString()};
667 /** Considers there is only one Display3D for each LayerSet. */
668 static public void remove(ProjectThing pt) {
669 if (null == pt) return;
670 if (null == pt.getObject()) return;
671 Object ob = pt.getObject();
672 if (!(ob instanceof Displayable)) return;
673 Displayable displ = (Displayable)ob;
674 Object d3ob = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
675 if (null == d3ob) {
676 // there is no Display3D showing the pt to remove
677 Utils.log2("No Display3D contains ProjectThing: " + pt);
678 return;
680 Display3D d3d = (Display3D)d3ob;
681 Object ob_mesh = d3d.ht_pt_meshes.remove(pt);
682 if (null == ob_mesh) {
683 Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
684 return; // not contained here
686 String title = makeTitle(displ);
687 //Utils.log(d3d.universe.contains(title) + ": Universe contains " + displ);
688 d3d.universe.removeContent(title); // WARNING if the title changes, problems: will need a table of pt vs title as it was when added to the universe. At the moment titles are not editable for basic types, but this may change in the future. TODO the future is here: titles are editable for basic types.
691 static private void writeTrianglesDXF(final StringBuffer sb, final List triangles, final String the_group, final String the_color) {
693 final char L = '\n';
694 final String s10 = "10\n"; final String s11 = "11\n"; final String s12 = "12\n"; final String s13 = "13\n";
695 final String s20 = "20\n"; final String s21 = "21\n"; final String s22 = "22\n"; final String s23 = "23\n";
696 final String s30 = "30\n"; final String s31 = "31\n"; final String s32 = "32\n"; final String s33 = "33\n";
697 final String triangle_header = "0\n3DFACE\n8\n" + the_group + "\n6\nCONTINUOUS\n62\n" + the_color + L;
699 final int len = triangles.size();
700 final Point3f[] vert = new Point3f[len];
701 triangles.toArray(vert);
702 for (int i=0; i<len; i+=3) {
704 sb.append(triangle_header)
706 .append(s10).append(vert[i].x).append(L)
707 .append(s20).append(vert[i].y).append(L)
708 .append(s30).append(vert[i].z).append(L)
710 .append(s11).append(vert[i+1].x).append(L)
711 .append(s21).append(vert[i+1].y).append(L)
712 .append(s31).append(vert[i+1].z).append(L)
714 .append(s12).append(vert[i+2].x).append(L)
715 .append(s22).append(vert[i+2].y).append(L)
716 .append(s32).append(vert[i+2].z).append(L)
718 .append(s13).append(vert[i+2].x).append(L) // repeated point
719 .append(s23).append(vert[i+2].y).append(L)
720 .append(s33).append(vert[i+2].z).append(L);
724 /** Creates a mesh for the given Displayable in a separate Thread. */
725 private Future<Content> addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
726 final double scale = this.scale;
727 FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
728 public Content call() {
729 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
730 try {
732 // 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.)
733 List triangles = null;
734 boolean no_culling = false; // don't show back faces when false
735 if (displ instanceof AreaList) {
736 int rs = resample;
737 if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
738 else rs = Display3D.this.resample;
739 triangles = ((AreaList)displ).generateTriangles(scale, rs);
740 //triangles = removeNonManifold(triangles);
741 } else if (displ instanceof Ball) {
742 double[][][] globe = Ball.generateGlobe(12, 12);
743 triangles = ((Ball)displ).generateTriangles(scale, globe);
744 } else if (displ instanceof Line3D) {
745 // Pipe and Polyline
746 // adjustResampling(); // fails horribly, needs first to correct mesh-generation code
747 triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
748 } else if (null == displ && pt.getType().equals("profile_list")) {
749 triangles = Profile.generateTriangles(pt, scale);
750 no_culling = true;
752 // safety checks
753 if (null == triangles) {
754 Utils.log("Some error ocurred: can't create triangles for " + displ);
755 return null;
757 if (0 == triangles.size()) {
758 Utils.log2("Skipping empty mesh for " + displ.getTitle());
759 return null;
761 if (0 != triangles.size() % 3) {
762 Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
763 return null;
765 Color color = null;
766 float alpha = 1.0f;
767 if (null != displ) {
768 color = displ.getColor();
769 alpha = displ.getAlpha();
770 } else {
771 // for profile_list: get from the first (what a kludge)
772 Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
773 if (null == obp) return null;
774 Displayable di = (Displayable)obp;
775 color = di.getColor();
776 alpha = di.getAlpha();
779 Content ct = null;
781 no_culling = true; // for ALL
783 // add to 3D view (synchronized)
784 synchronized (u_lock) {
785 u_lock.lock();
786 try {
787 // craft a unique title (id is always unique)
788 String title = null == displ ? pt.toString() + " #" + pt.getId() : makeTitle(displ);
789 if (ht_pt_meshes.contains(pt) || universe.contains(title)) {
790 // remove content from universe
791 universe.removeContent(title);
792 // no need to remove entry from table, it's overwritten below
794 // register mesh
795 ht_pt_meshes.put(pt, triangles);
796 // ensure proper default transform
797 //universe.resetView();
799 Color3f c3 = new Color3f(color);
801 if (no_culling) {
802 // create a mesh with the same color and zero transparency (that is, full opacity)
803 CustomTriangleMesh mesh = new CustomTriangleMesh(triangles, c3, 0);
804 // Set mesh properties for double-sided triangles
805 PolygonAttributes pa = mesh.getAppearance().getPolygonAttributes();
806 pa.setCullFace(PolygonAttributes.CULL_NONE);
807 pa.setBackFaceNormalFlip(true);
808 mesh.setColor(c3);
809 // After setting properties, add to the viewer
810 ct = universe.addCustomMesh(mesh, title);
811 } else {
812 ct = universe.addTriangleMesh(triangles, c3, title);
815 if (null == ct) return null;
817 // Set general content properties
818 ct.setTransparency(1f - alpha);
819 // Default is unlocked (editable) transformation; set it to locked:
820 ct.toggleLock();
822 } catch (Exception e) {
823 IJError.print(e);
825 u_lock.unlock();
828 Utils.log2(pt.toString() + " n points: " + triangles.size());
830 return ct;
832 } catch (Exception e) {
833 IJError.print(e);
834 return null;
837 }});
838 executors.submit(fu);
839 return fu;
842 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
843 static public Future<Content> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
844 return addMesh(ref_ls, vs, title, color, null, 1.0f);
847 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
848 static public Future<Content> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha) {
849 final FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
850 public Content call() {
851 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
852 try {
853 /////
854 final Display3D d3d = Display3D.get(ref_ls);
855 final double scale = d3d.scale;
856 final double width = d3d.width;
857 float transp = 1 - alpha;
858 if (transp < 0) transp = 0;
859 if (transp > 1) transp = 1;
860 if (1 == transp) {
861 Utils.log("WARNING: adding a 3D object fully transparent.");
864 double[] wi = widths;
865 if (null == widths) {
866 wi = new double[vs.getPoints(0).length];
867 //Utils.log2("len: " + wi.length + vs.getPoints(0).length + vs.getPoints(1).length);
868 Arrays.fill(wi, 2.0);
869 } else if (widths.length != vs.length()) {
870 Utils.log("ERROR: widths.length != VectorString3D.length()");
871 return null;
874 List triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), scale);
876 Content ct = null;
878 // add to 3D view (synchronized)
879 synchronized (d3d.u_lock) {
880 d3d.u_lock.lock();
881 try {
882 // ensure proper default transform
883 //d3d.universe.resetView();
885 //Utils.log2(title + " : vertex count % 3 = " + triangles.size() % 3 + " for " + triangles.size() + " vertices");
886 //d3d.universe.ensureScale((float)(width*scale));
887 ct = d3d.universe.addMesh(triangles, new Color3f(color), title, /*(float)(width*scale),*/ 1);
888 ct.setTransparency(transp);
889 ct.toggleLock();
890 } catch (Exception e) {
891 IJError.print(e);
893 d3d.u_lock.unlock();
896 return ct;
898 /////
899 } catch (Exception e) {
900 IJError.print(e);
901 return null;
904 }});
907 launchers.submit(new Runnable() { public void run() {
908 final Display3D d3d = Display3D.get(ref_ls);
909 d3d.executors.submit(fu);
910 }});
912 return fu;
915 // This method has the exclusivity in adjusting the resampling value.
916 synchronized private final int adjustResampling() {
917 if (resample > 0) return resample;
918 final GenericDialog gd = new GenericDialog("Resample");
919 gd.addSlider("Resample: ", 1, 20, -1 != resample ? resample : DEFAULT_RESAMPLE);
920 gd.showDialog();
921 if (gd.wasCanceled()) {
922 resample = -1 != resample ? resample : DEFAULT_RESAMPLE; // current or default value
923 return resample;
925 resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
926 return resample;
929 /** Checks if there is any Display3D instance currently showing the given Displayable. */
930 static public boolean isDisplayed(final Displayable d) {
931 if (null == d) return false;
932 final String title = makeTitle(d);
933 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
934 Display3D d3d = (Display3D)it.next();
935 if (null != d3d.universe.getContent(title)) return true;
937 if (d.getClass() == Profile.class) {
938 Content content = getProfileContent(d);
940 return false;
943 /** 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. */
944 static public Content getProfileContent(final Displayable d) {
945 if (null == d) return null;
946 if (d.getClass() != Profile.class) return null;
947 Display3D d3d = get(d.getLayer().getParent());
948 if (null == d3d) return null;
949 ProjectThing pt = d.getProject().findProjectThing(d);
950 if (null == pt) return null;
951 pt = (ProjectThing) pt.getParent();
952 return d3d.universe.getContent(new StringBuffer(pt.toString()).append(" #").append(pt.getId()).toString());
955 static public void setColor(final Displayable d, final Color color) {
956 launchers.submit(new Runnable() { public void run() {
957 final Display3D d3d = getDisplay(d.getLayer().getParent());
958 if (null == d3d) return; // no 3D displays open
959 d3d.executors.submit(new Runnable() { public void run() {
960 Content content = d3d.universe.getContent(makeTitle(d));
961 if (null == content) content = getProfileContent(d);
962 if (null != content) content.setColor(new Color3f(color));
963 }});
964 }});
967 static public void setTransparency(final Displayable d, final float alpha) {
968 if (null == d) return;
969 Layer layer = d.getLayer();
970 if (null == layer) return; // some objects have no layer, such as the parent LayerSet.
971 Object ob = ht_layer_sets.get(layer.getParent());
972 if (null == ob) return;
973 Display3D d3d = (Display3D)ob;
974 String title = makeTitle(d);
975 Content content = d3d.universe.getContent(title);
976 if (null == content) content = getProfileContent(d);
977 if (null != content) content.setTransparency(1 - alpha);
978 else if (null == content && d.getClass().equals(Patch.class)) {
979 Patch pa = (Patch)d;
980 if (pa.isStack()) {
981 title = pa.getProject().getLoader().getFileName(pa);
982 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
983 d3d = (Display3D)it.next();
984 for (Iterator cit = d3d.universe.getContents().iterator(); cit.hasNext(); ) {
985 Content c = (Content)cit.next();
986 if (c.getName().startsWith(title)) {
987 c.setTransparency(1 - alpha);
988 // no break, since there could be a volume and an orthoslice
996 static public String makeTitle(final Displayable d) {
997 return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
999 static public String makeTitle(final Patch p) {
1000 return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
1001 + " #" + p.getProject().getLoader().getNextId();
1004 /** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
1005 * (otherwise returns null). */
1006 static public Future<Content> update(final Displayable d) {
1007 Layer layer = d.getLayer();
1008 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
1009 Object ob = ht_layer_sets.get(layer.getParent());
1010 if (null == ob) return null;
1011 Display3D d3d = (Display3D)ob;
1012 return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
1016 static public final double computeTriangleArea() {
1017 return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
1018 Math.pow(yA*zB + yB*zC + yC*zA, 2) +
1019 Math.pow(zA*xB + zB*xC + zC*xA, 2));
1023 static public final boolean contains(final LayerSet ls, final String title) {
1024 final Display3D d3d = getDisplay(ls);
1025 if (null == d3d) return false;
1026 return null != d3d.universe.getContent(title);
1029 static public void destroy() {
1030 launchers.shutdownNow();
1033 static public void init() {
1034 if (launchers.isShutdown()) {
1035 launchers = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());