Removing debug message, and minor optimizations.
[trakem2.git] / ini / trakem2 / display / Display3D.java
blob114ba2f5387d4f494dfbf0a990ca91dc66d41567
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 show(pt, true, -1);
354 Display3D.resetView(pt.getProject().getRootLayerSet());
355 }}.start();
358 /** 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. */
359 static public Future<List<Content>> show(final ProjectThing pt, final boolean wait, final int resample) {
360 if (null == pt) return null;
361 final Callable<List<Content>> c = new Callable<List<Content>>() {
362 public List<Content> call() {
363 try {
364 // scan the given ProjectThing for 3D-viewable items not present in the ht_meshes
365 // So: find arealist, pipe, ball, and profile_list types
366 final HashSet hs = pt.findBasicTypeChildren();
367 if (null == hs || 0 == hs.size()) {
368 Utils.log("Node " + pt + " contains no 3D-displayable children");
369 return null;
372 final List<Content> list = new ArrayList<Content>();
374 for (final Iterator it = hs.iterator(); it.hasNext(); ) {
375 // obtain the Displayable object under the node
376 final ProjectThing child = (ProjectThing)it.next();
377 Object obc = child.getObject();
378 Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
379 if (null != displ) {
380 if (displ.getClass().equals(Profile.class)) {
381 //Utils.log("Display3D can't handle Bezier profiles at the moment.");
382 // handled by profile_list Thing
383 continue;
385 if (!displ.isVisible()) {
386 Utils.log("Skipping non-visible node " + displ);
387 continue;
390 //StopWatch sw = new StopWatch();
391 // obtain the containing LayerSet
392 Display3D d3d = null;
393 if (null != displ) d3d = Display3D.get(displ.getLayerSet());
394 else if (child.getType().equals("profile_list")) {
395 ArrayList al_children = child.getChildren();
396 if (null == al_children || 0 == al_children.size()) continue;
397 // else, get the first Profile and get its LayerSet
398 d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
399 } else {
400 Utils.log("Don't know what to do with node " + child);
402 if (null == d3d) {
403 Utils.log("Could not get a proper 3D display for node " + displ);
404 return null; // java3D not installed most likely
406 if (d3d.ht_pt_meshes.contains(child)) {
407 Utils.log2("Already here: " + child);
408 continue; // already here
410 setWaitingCursor(); // the above may be creating a display
411 //sw.elapsed("after creating and/or retrieving Display3D");
412 Future<Content> fu = d3d.addMesh(child, displ, resample);
413 if (wait && -1 != d3d.resample) {
414 list.add(fu.get());
417 //sw.elapsed("after creating mesh");
420 return list;
422 } catch (Exception e) {
423 IJError.print(e);
424 return null;
425 } finally {
426 doneWaiting();
430 return launchers.submit(c);
433 static public void resetView(final LayerSet ls) {
434 Display3D d3d = (Display3D) ht_layer_sets.get(ls);
435 if (null != d3d) d3d.universe.resetView();
438 static public void showOrthoslices(Patch p) {
439 Display3D d3d = get(p.getLayerSet());
440 d3d.adjustResampling();
441 //d3d.universe.resetView();
442 String title = makeTitle(p) + " orthoslices";
443 // remove if present
444 d3d.universe.removeContent(title);
445 PatchStack ps = p.makePatchStack();
446 ImagePlus imp = get8BitStack(ps);
447 d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
448 Content ct = d3d.universe.getContent(title);
449 setTransform(ct, ps.getPatch(0));
450 ct.toggleLock(); // locks the added content
453 static public void showVolume(Patch p) {
454 Display3D d3d = get(p.getLayerSet());
455 d3d.adjustResampling();
456 //d3d.universe.resetView();
457 String title = makeTitle(p) + " volume";
458 // remove if present
459 d3d.universe.removeContent(title);
460 PatchStack ps = p.makePatchStack();
461 ImagePlus imp = get8BitStack(ps);
462 d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
463 Content ct = d3d.universe.getContent(title);
464 setTransform(ct, ps.getPatch(0));
465 ct.toggleLock(); // locks the added content
468 static private void setTransform(Content ct, Patch p) {
469 final double[] a = new double[6];
470 p.getAffineTransform().getMatrix(a);
471 Calibration cal = p.getLayerSet().getCalibration();
472 // a is: m00 m10 m01 m11 m02 m12
473 // d expects: m01 m02 m03 m04, m11 m12 ...
474 ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
475 a[1], a[3], 0, a[5] * cal.pixelWidth,
476 0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
477 0, 0, 0, 1}));
480 /** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
481 * If the PatchStach is already of the right type, it is returned,
482 * otherwise a copy is made in the proper type.
484 static private ImagePlus get8BitStack(final PatchStack ps) {
485 switch (ps.getType()) {
486 case ImagePlus.COLOR_RGB:
487 // convert stack to 8-bit color
488 return ps.createColor256Copy();
489 case ImagePlus.GRAY16:
490 case ImagePlus.GRAY32:
491 // convert stack to 8-bit
492 return ps.createGray8Copy();
493 default:
494 return ps;
498 /** A Material, but avoiding name colisions. */
499 static private int mat_index = 1;
500 static private class Mtl {
501 float alpha = 1;
502 float R = 1;
503 float G = 1;
504 float B = 1;
505 String name;
506 Mtl(float alpha, float R, float G, float B) {
507 this.alpha = alpha;
508 this.R = R;
509 this.G = G;
510 this.B = B;
511 name = "mat_" + mat_index;
512 mat_index++;
514 public boolean equals(Object ob) {
515 if (ob instanceof Display3D.Mtl) {
516 Mtl mat = (Mtl)ob;
517 if (mat.alpha == alpha
518 && mat.R == R
519 && mat.G == G
520 && mat.B == B) {
521 return true;
524 return false;
526 void fill(StringBuffer sb) {
527 sb.append("\nnewmtl ").append(name).append('\n')
528 .append("Ns 96.078431\n")
529 .append("Ka 0.0 0.0 0.0\n")
530 .append("Kd ").append(R).append(' ').append(G).append(' ').append(B).append('\n') // this is INCORRECT but I'll figure out the conversion later
531 .append("Ks 0.5 0.5 0.5\n")
532 .append("Ni 1.0\n")
533 .append("d ").append(alpha).append('\n')
534 .append("illum 2\n\n");
536 int getAsSingle() {
537 return (int)((R + G + B) / 3 * 255); // something silly
541 /** Generates DXF file from a table of ProjectThing and their associated triangles. */
542 private String createDXF(Hashtable ht_content) {
543 StringBuffer sb_data = new StringBuffer("0\nSECTION\n2\nENTITIES\n"); //header of file
544 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
545 Map.Entry entry = (Map.Entry)it.next();
546 ProjectThing pt = (ProjectThing)entry.getKey();
547 Displayable displ = (Displayable)pt.getObject();
548 List triangles = (List)entry.getValue();
549 float[] color = displ.getColor().getColorComponents(null);
550 Mtl mtl = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
551 writeTrianglesDXF(sb_data, triangles, mtl.name, Integer.toString(mtl.getAsSingle()));
553 sb_data.append("0\nENDSEC\n0\nEOF\n"); //TRAILER of the file
554 return sb_data.toString();
557 /** @param format works as extension as well. */
558 private void export(final ProjectThing pt, final String format) {
559 if (0 == ht_pt_meshes.size()) return;
560 // select file
561 File file = Utils.chooseFile("untitled", format);
562 if (null == file) return;
563 final String name = file.getName();
564 String name2 = name;
565 if (!name2.endsWith("." + format)) {
566 name2 += "." + format;
568 File f2 = new File(file.getParent() + "/" + name2);
569 int i = 1;
570 while (f2.exists()) {
571 name2 = name + "_" + i + "." + format;
572 f2 = new File(name2);
574 Hashtable ht_content = ht_pt_meshes;
575 if (null != pt) {
576 ht_content = new Hashtable();
577 ht_content.put(pt, ht_pt_meshes.get(pt));
579 if (format.equals("obj")) {
580 String[] data = createObjAndMtl(name2, ht_content);
581 Utils.saveToFile(f2, data[0]);
582 Utils.saveToFile(new File(f2.getParent() + "/" + name2 + ".mtl"), data[1]);
583 } else if (format.equals("dxf")) {
584 Utils.saveToFile(f2, createDXF(ht_content));
588 /** Wavefront format. Returns the contents of two files: one for materials, another for meshes*/
589 private String[] createObjAndMtl(final String file_name, final Hashtable ht_content) {
590 StringBuffer sb_obj = new StringBuffer("# TrakEM2 OBJ File\n");
591 sb_obj.append("mtllib ").append(file_name).append(".mtl").append('\n');
593 Hashtable ht_mat = new Hashtable();
595 int j = 1; // Vert indices in .obj files are global, not reset for every object.
596 // starting at '1' because vert indices start at one.
598 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
599 Map.Entry entry = (Map.Entry)it.next(); // I hate java's gratuituous verbosity
600 ProjectThing pt = (ProjectThing)entry.getKey();
601 Displayable displ = (Displayable)pt.getObject();
602 List triangles = (List)entry.getValue();
603 // make material, and see whether it exists already
604 float[] color = displ.getColor().getColorComponents(null);
605 Mtl mat = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
606 Object mat2 = ht_mat.get(mat);
607 if (null != mat2) mat = (Mtl)mat2; // recycling
608 else ht_mat.put(mat, mat); // !@#$% Can't get the object in a HashSet easily
609 // make list of vertices
610 String title = displ.getProject().getMeaningfulTitle(displ).replaceAll(" ", "_").replaceAll("#", "--");
611 Hashtable ht_points = new Hashtable(); // because we like inefficiency
612 sb_obj.append("o ").append(title).append('\n');
613 final int len = triangles.size();
614 int[] index = new int[len];
615 int k = 0; // iterate over index array, to make faces later
616 // j is tag for each new vert, which start at 1 (for some ridiculous reason)
617 for (Iterator tt = triangles.iterator(); tt.hasNext(); ) {
618 Point3f p = (Point3f)tt.next();
619 //no need if coords are not displaced//p = (Point3f)p.clone();
620 // check if point already exists
621 Object ob = ht_points.get(p);
622 if (null != ob) {
623 index[k] = ((Integer)ob).intValue();
624 } else {
625 // new point
626 index[k] = j;
627 // record
628 ht_points.put(p, new Integer(j));
629 // append vertex
630 sb_obj.append('v').append(' ').append(p.x)
631 .append(' ').append(p.y)
632 .append(' ').append(p.z).append('\n');
633 j++;
635 k++;
637 sb_obj.append("usemtl ").append(mat.name).append('\n');
638 sb_obj.append("s 1\n");
639 if (0 != len % 3) Utils.log2("WARNING: list of triangles not multiple of 3");
640 // print faces
641 int len_p = ht_points.size();
642 for (int i=0; i<len; i+=3) {
643 sb_obj.append('f').append(' ').append(index[i])
644 .append(' ').append(index[i+1])
645 .append(' ').append(index[i+2]).append('\n');
646 //if (index[i] > len_p) Utils.log2("WARNING: face vert index beyond range"); // range is from 1 to len_p inclusive
647 //if (index[i+1] > len_p) Utils.log2("WARNING: face vert index beyond range");
648 //if (index[i+2] > len_p) Utils.log2("WARNING: face vert index beyond range");
649 //Utils.log2("j: " + index[i]);
650 // checks passed
652 sb_obj.append('\n');
654 // make mtl file
655 StringBuffer sb_mtl = new StringBuffer("# TrakEM2 MTL File\n");
656 for (Iterator it = ht_mat.keySet().iterator(); it.hasNext(); ) {
657 Mtl mat = (Mtl)it.next();
658 mat.fill(sb_mtl);
661 return new String[]{sb_obj.toString(), sb_mtl.toString()};
664 /** Considers there is only one Display3D for each LayerSet. */
665 static public void remove(ProjectThing pt) {
666 if (null == pt) return;
667 if (null == pt.getObject()) return;
668 Object ob = pt.getObject();
669 if (!(ob instanceof Displayable)) return;
670 Displayable displ = (Displayable)ob;
671 Object d3ob = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
672 if (null == d3ob) {
673 // there is no Display3D showing the pt to remove
674 Utils.log2("No Display3D contains ProjectThing: " + pt);
675 return;
677 Display3D d3d = (Display3D)d3ob;
678 Object ob_mesh = d3d.ht_pt_meshes.remove(pt);
679 if (null == ob_mesh) {
680 Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
681 return; // not contained here
683 String title = makeTitle(displ);
684 //Utils.log(d3d.universe.contains(title) + ": Universe contains " + displ);
685 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.
688 static private void writeTrianglesDXF(final StringBuffer sb, final List triangles, final String the_group, final String the_color) {
690 final char L = '\n';
691 final String s10 = "10\n"; final String s11 = "11\n"; final String s12 = "12\n"; final String s13 = "13\n";
692 final String s20 = "20\n"; final String s21 = "21\n"; final String s22 = "22\n"; final String s23 = "23\n";
693 final String s30 = "30\n"; final String s31 = "31\n"; final String s32 = "32\n"; final String s33 = "33\n";
694 final String triangle_header = "0\n3DFACE\n8\n" + the_group + "\n6\nCONTINUOUS\n62\n" + the_color + L;
696 final int len = triangles.size();
697 final Point3f[] vert = new Point3f[len];
698 triangles.toArray(vert);
699 for (int i=0; i<len; i+=3) {
701 sb.append(triangle_header)
703 .append(s10).append(vert[i].x).append(L)
704 .append(s20).append(vert[i].y).append(L)
705 .append(s30).append(vert[i].z).append(L)
707 .append(s11).append(vert[i+1].x).append(L)
708 .append(s21).append(vert[i+1].y).append(L)
709 .append(s31).append(vert[i+1].z).append(L)
711 .append(s12).append(vert[i+2].x).append(L)
712 .append(s22).append(vert[i+2].y).append(L)
713 .append(s32).append(vert[i+2].z).append(L)
715 .append(s13).append(vert[i+2].x).append(L) // repeated point
716 .append(s23).append(vert[i+2].y).append(L)
717 .append(s33).append(vert[i+2].z).append(L);
721 /** Creates a mesh for the given Displayable in a separate Thread. */
722 private Future<Content> addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
723 final double scale = this.scale;
724 FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
725 public Content call() {
726 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
727 try {
729 // 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.)
730 List triangles = null;
731 boolean no_culling = false; // don't show back faces when false
732 if (displ instanceof AreaList) {
733 int rs = resample;
734 if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
735 else rs = Display3D.this.resample;
736 triangles = ((AreaList)displ).generateTriangles(scale, rs);
737 //triangles = removeNonManifold(triangles);
738 } else if (displ instanceof Ball) {
739 double[][][] globe = Ball.generateGlobe(12, 12);
740 triangles = ((Ball)displ).generateTriangles(scale, globe);
741 } else if (displ instanceof Line3D) {
742 // Pipe and Polyline
743 // adjustResampling(); // fails horribly, needs first to correct mesh-generation code
744 triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
745 } else if (null == displ && pt.getType().equals("profile_list")) {
746 triangles = Profile.generateTriangles(pt, scale);
747 no_culling = true;
749 // safety checks
750 if (null == triangles) {
751 Utils.log("Some error ocurred: can't create triangles for " + displ);
752 return null;
754 if (0 == triangles.size()) {
755 Utils.log2("Skipping empty mesh for " + displ.getTitle());
756 return null;
758 if (0 != triangles.size() % 3) {
759 Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
760 return null;
762 Color color = null;
763 float alpha = 1.0f;
764 if (null != displ) {
765 color = displ.getColor();
766 alpha = displ.getAlpha();
767 } else {
768 // for profile_list: get from the first (what a kludge)
769 Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
770 if (null == obp) return null;
771 Displayable di = (Displayable)obp;
772 color = di.getColor();
773 alpha = di.getAlpha();
776 Content ct = null;
778 no_culling = true; // for ALL
780 // add to 3D view (synchronized)
781 synchronized (u_lock) {
782 u_lock.lock();
783 try {
784 // craft a unique title (id is always unique)
785 String title = null == displ ? pt.toString() + " #" + pt.getId() : makeTitle(displ);
786 if (ht_pt_meshes.contains(pt) || universe.contains(title)) {
787 // remove content from universe
788 universe.removeContent(title);
789 // no need to remove entry from table, it's overwritten below
791 // register mesh
792 ht_pt_meshes.put(pt, triangles);
793 // ensure proper default transform
794 //universe.resetView();
796 Color3f c3 = new Color3f(color);
798 if (no_culling) {
799 // create a mesh with the same color and zero transparency (that is, full opacity)
800 CustomTriangleMesh mesh = new CustomTriangleMesh(triangles, c3, 0);
801 // Set mesh properties for double-sided triangles
802 PolygonAttributes pa = mesh.getAppearance().getPolygonAttributes();
803 pa.setCullFace(PolygonAttributes.CULL_NONE);
804 pa.setBackFaceNormalFlip(true);
805 mesh.setColor(c3);
806 // After setting properties, add to the viewer
807 ct = universe.addCustomMesh(mesh, title);
808 } else {
809 ct = universe.addTriangleMesh(triangles, c3, title);
812 if (null == ct) return null;
814 // Set general content properties
815 ct.setTransparency(1f - alpha);
816 // Default is unlocked (editable) transformation; set it to locked:
817 ct.toggleLock();
819 } catch (Exception e) {
820 IJError.print(e);
822 u_lock.unlock();
825 Utils.log2(pt.toString() + " n points: " + triangles.size());
827 return ct;
829 } catch (Exception e) {
830 IJError.print(e);
831 return null;
834 }});
835 executors.submit(fu);
836 return fu;
839 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
840 static public Future<Content> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
841 return addMesh(ref_ls, vs, title, color, null, 1.0f);
844 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
845 static public Future<Content> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha) {
846 final FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
847 public Content call() {
848 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
849 try {
850 /////
851 final Display3D d3d = Display3D.get(ref_ls);
852 final double scale = d3d.scale;
853 final double width = d3d.width;
854 float transp = 1 - alpha;
855 if (transp < 0) transp = 0;
856 if (transp > 1) transp = 1;
857 if (1 == transp) {
858 Utils.log("WARNING: adding a 3D object fully transparent.");
861 double[] wi = widths;
862 if (null == widths) {
863 wi = new double[vs.getPoints(0).length];
864 //Utils.log2("len: " + wi.length + vs.getPoints(0).length + vs.getPoints(1).length);
865 Arrays.fill(wi, 2.0);
866 } else if (widths.length != vs.length()) {
867 Utils.log("ERROR: widths.length != VectorString3D.length()");
868 return null;
871 List triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), scale);
873 Content ct = null;
875 // add to 3D view (synchronized)
876 synchronized (d3d.u_lock) {
877 d3d.u_lock.lock();
878 try {
879 // ensure proper default transform
880 //d3d.universe.resetView();
882 //Utils.log2(title + " : vertex count % 3 = " + triangles.size() % 3 + " for " + triangles.size() + " vertices");
883 //d3d.universe.ensureScale((float)(width*scale));
884 ct = d3d.universe.addMesh(triangles, new Color3f(color), title, /*(float)(width*scale),*/ 1);
885 ct.setTransparency(transp);
886 ct.toggleLock();
887 } catch (Exception e) {
888 IJError.print(e);
890 d3d.u_lock.unlock();
893 return ct;
895 /////
896 } catch (Exception e) {
897 IJError.print(e);
898 return null;
901 }});
904 launchers.submit(new Runnable() { public void run() {
905 final Display3D d3d = Display3D.get(ref_ls);
906 d3d.executors.submit(fu);
907 }});
909 return fu;
912 // This method has the exclusivity in adjusting the resampling value.
913 synchronized private final int adjustResampling() {
914 if (resample > 0) return resample;
915 final GenericDialog gd = new GenericDialog("Resample");
916 gd.addSlider("Resample: ", 1, 20, -1 != resample ? resample : DEFAULT_RESAMPLE);
917 gd.showDialog();
918 if (gd.wasCanceled()) {
919 resample = -1 != resample ? resample : DEFAULT_RESAMPLE; // current or default value
920 return resample;
922 resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
923 return resample;
926 /** Checks if there is any Display3D instance currently showing the given Displayable. */
927 static public boolean isDisplayed(final Displayable d) {
928 if (null == d) return false;
929 final String title = makeTitle(d);
930 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
931 Display3D d3d = (Display3D)it.next();
932 if (null != d3d.universe.getContent(title)) return true;
934 if (d.getClass() == Profile.class) {
935 Content content = getProfileContent(d);
937 return false;
940 /** 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. */
941 static public Content getProfileContent(final Displayable d) {
942 if (null == d) return null;
943 if (d.getClass() != Profile.class) return null;
944 Display3D d3d = get(d.getLayer().getParent());
945 if (null == d3d) return null;
946 ProjectThing pt = d.getProject().findProjectThing(d);
947 if (null == pt) return null;
948 pt = (ProjectThing) pt.getParent();
949 return d3d.universe.getContent(new StringBuffer(pt.toString()).append(" #").append(pt.getId()).toString());
952 static public void setColor(final Displayable d, final Color color) {
953 launchers.submit(new Runnable() { public void run() {
954 final Display3D d3d = getDisplay(d.getLayer().getParent());
955 if (null == d3d) return; // no 3D displays open
956 d3d.executors.submit(new Runnable() { public void run() {
957 Content content = d3d.universe.getContent(makeTitle(d));
958 if (null == content) content = getProfileContent(d);
959 if (null != content) content.setColor(new Color3f(color));
960 }});
961 }});
964 static public void setTransparency(final Displayable d, final float alpha) {
965 if (null == d) return;
966 Layer layer = d.getLayer();
967 if (null == layer) return; // some objects have no layer, such as the parent LayerSet.
968 Object ob = ht_layer_sets.get(layer.getParent());
969 if (null == ob) return;
970 Display3D d3d = (Display3D)ob;
971 String title = makeTitle(d);
972 Content content = d3d.universe.getContent(title);
973 if (null == content) content = getProfileContent(d);
974 if (null != content) content.setTransparency(1 - alpha);
975 else if (null == content && d.getClass().equals(Patch.class)) {
976 Patch pa = (Patch)d;
977 if (pa.isStack()) {
978 title = pa.getProject().getLoader().getFileName(pa);
979 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
980 d3d = (Display3D)it.next();
981 for (Iterator cit = d3d.universe.getContents().iterator(); cit.hasNext(); ) {
982 Content c = (Content)cit.next();
983 if (c.getName().startsWith(title)) {
984 c.setTransparency(1 - alpha);
985 // no break, since there could be a volume and an orthoslice
993 static public String makeTitle(final Displayable d) {
994 return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
996 static public String makeTitle(final Patch p) {
997 return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
998 + " #" + p.getProject().getLoader().getNextId();
1001 /** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
1002 * (otherwise returns null). */
1003 static public Future<Content> update(final Displayable d) {
1004 Layer layer = d.getLayer();
1005 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
1006 Object ob = ht_layer_sets.get(layer.getParent());
1007 if (null == ob) return null;
1008 Display3D d3d = (Display3D)ob;
1009 return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
1013 static public final double computeTriangleArea() {
1014 return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
1015 Math.pow(yA*zB + yB*zC + yC*zA, 2) +
1016 Math.pow(zA*xB + zB*xC + zC*xA, 2));
1020 static public final boolean contains(final LayerSet ls, final String title) {
1021 final Display3D d3d = getDisplay(ls);
1022 if (null == d3d) return false;
1023 return null != d3d.universe.getContent(title);
1026 static public void destroy() {
1027 launchers.shutdownNow();
1030 static public void init() {
1031 if (launchers.isShutdown()) {
1032 launchers = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());