Moved Display3D to use Executors.
[trakem2.git] / ini / trakem2 / display / Display3D.java
blobb7b6e6ea628d5085791752db9b5f21606a04566c
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 /** The threading is so poorly done ... it BARELY works, it's fragile. */
69 /** Table of LayerSet and Display3D - since there is a one to one relationship. */
70 static private Hashtable ht_layer_sets = new Hashtable();
71 /**Control calls to new Display3D. */
72 static private Lock htlock = new Lock();
74 /** 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. */
75 static public Hashtable getMasterTable() {
76 return (Hashtable)ht_layer_sets.clone();
79 /** Table of ProjectThing keys versus meshes, the latter represented by List of triangles in the form of thre econsecutive Point3f in the List.*/
80 private Hashtable ht_pt_meshes = new Hashtable();
82 private Image3DUniverse universe;
84 private Lock u_lock = new Lock();
86 private LayerSet layer_set;
87 private double width, height;
88 private int resample = -1; // unset
89 static private final int DEFAULT_RESAMPLE = 4;
90 /** If the LayerSet dimensions are too large, then limit to max 2048 for width or height and setup a scale.*/
91 private double scale = 1.0;
92 static private final int MAX_DIMENSION = 1024;
94 private String selected = null;
96 // To fork away from the EventDispatchThread
97 static private ExecutorService launchers = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
99 // To build meshes
100 private ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
103 static private KeyAdapter ka = new KeyAdapter() {
104 public void keyPressed(KeyEvent ke) {
105 // F1 .. F12 keys to set tools
106 ProjectToolbar.keyPressed(ke);
111 /** Defaults to parallel projection. */
112 private Display3D(final LayerSet ls) {
113 this.layer_set = ls;
114 this.universe = new Image3DUniverse(512, 512); // size of the initial canvas, not the universe itself
115 this.universe.getViewer().getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); // (View.PERSPECTIVE_PROJECTION);
116 computeScale(ls);
117 this.universe.show();
118 this.universe.getWindow().addWindowListener(new IW3DListener(this, ls));
119 // it ignores the listeners:
120 //preaddKeyListener(this.universe.getWindow(), ka);
121 //preaddKeyListener(this.universe.getWindow().getCanvas(), ka);
123 // register
124 Display3D.ht_layer_sets.put(ls, this);
128 private void preaddKeyListener(Component c, KeyListener kl) {
129 KeyListener[] all = c.getKeyListeners();
130 if (null != all) {
131 for (KeyListener k : all) c.removeKeyListener(k);
133 c.addKeyListener(kl);
134 if (null != all) {
135 for (KeyListener k : all) c.addKeyListener(k);
140 public Image3DUniverse getUniverse() {
141 return universe;
144 /* Take a snapshot know-it-all mode. Each Transform3D given as argument gets assigned to the (nearly) homonimous TransformGroup, which have the following relationships:
146 * scaleTG contains rotationsTG
147 * rotationsTG contains translateTG
148 * translateTG contains centerTG
149 * centerTG contains the whole scene, with all meshes, etc.
151 * Any null arguments imply the current transform in the open Display3D.
153 * 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.
155 * The TransformGroup instances may be reached like this:
157 * LayerSet layer_set = Display.getFrontLayer().getParent();
158 * Display3D d3d = Display3D.getDisplay(layer_set);
159 * TransformGroup scaleTG = d3d.getUniverse().getGlobalScale();
160 * TransformGroup rotationsTG = d3d.getUniverse().getGlobalRotate();
161 * TransformGroup translateTG = d3d.getUniverse().getGlobalTranslate();
162 * TransformGroup centerTG = d3d.getUniverse().getCenterTG();
164 * ... and the Transform3D from each may be read out indirectly like this:
166 * Transform3D t_scale = new Transform3D();
167 * scaleTG.getTransform(t_scale);
168 * ...
170 * 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).
173 /*public ImagePlus makeSnapshot(final Transform3D scale, final Transform3D rotate, final Transform3D translate, final Transform3D center) {
174 return universe.makeSnapshot(scale, rotate, translate, center);
177 /** Uses current scaling, translation and centering transforms! */
178 /*public ImagePlus makeSnapshotXY() { // aka posterior
179 // default view
180 return universe.makeSnapshot(null, new Transform3D(), null, null);
182 /** Uses current scaling, translation and centering transforms! */
183 /*public ImagePlus makeSnapshotXZ() { // aka dorsal
184 Transform3D rot1 = new Transform3D();
185 rot1.rotZ(-Math.PI/2);
186 Transform3D rot2 = new Transform3D();
187 rot2.rotX(Math.PI/2);
188 rot1.mul(rot2);
189 return universe.makeSnapshot(null, rot1, null, null);
192 /** Uses current scaling, translation and centering transforms! */
194 public ImagePlus makeSnapshotYZ() { // aka lateral
195 Transform3D rot = new Transform3D();
196 rot.rotY(Math.PI/2);
197 return universe.makeSnapshot(null, rot, null, null);
201 public ImagePlus makeSnapshotZX() { // aka frontal
202 Transform3D rot = new Transform3D();
203 rot.rotX(-Math.PI/2);
204 return universe.makeSnapshot(null, rot, null, null);
208 /** Uses current scaling, translation and centering transforms! Opposite side of XZ. */
210 public ImagePlus makeSnapshotXZOpp() {
211 Transform3D rot1 = new Transform3D();
212 rot1.rotX(-Math.PI/2); // 90 degrees clockwise
213 Transform3D rot2 = new Transform3D();
214 rot2.rotY(Math.PI); // 180 degrees around Y, to the other side.
215 rot1.mul(rot2);
216 return universe.makeSnapshot(null, rot1, null, null);
219 private class IW3DListener extends WindowAdapter {
220 private Display3D d3d;
221 private LayerSet ls;
222 IW3DListener(Display3D d3d, LayerSet ls) {
223 this.d3d = d3d;
224 this.ls = ls;
226 public void windowClosing(WindowEvent we) {
227 Utils.log2("Display3D.windowClosing");
228 d3d.executors.shutdownNow();
229 /*Object ob =*/ ht_layer_sets.remove(ls);
230 /*if (null != ob) {
231 Utils.log2("Removed Display3D from table for LayerSet " + ls);
234 public void windowClosed(WindowEvent we) {
235 Utils.log2("Display3D.windowClosed");
236 ht_layer_sets.remove(ls);
240 /** Reads the #ID in the name, which is immutable. */
241 private ProjectThing find(String name) {
242 long id = Long.parseLong(name.substring(name.lastIndexOf('#')+1));
243 for (Iterator it = ht_pt_meshes.keySet().iterator(); it.hasNext(); ) {
244 ProjectThing pt = (ProjectThing)it.next();
245 Displayable d = (Displayable)pt.getObject();
246 if (d.getId() == id) {
247 return pt;
250 return null;
253 /** 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. */
254 private void computeScale(LayerSet ls) {
255 this.width = ls.getLayerWidth();
256 this.height = ls.getLayerHeight();
257 if (width > MAX_DIMENSION) {
258 scale = MAX_DIMENSION / width;
259 height *= scale;
260 width = MAX_DIMENSION;
262 if (height > MAX_DIMENSION) {
263 scale = MAX_DIMENSION / height;
264 width *= scale;
265 height = MAX_DIMENSION;
267 //Utils.log2("scale, width, height: " + scale + ", " + width + ", " + height);
270 static private boolean check_j3d = true;
271 static private boolean has_j3d_3dviewer = false;
273 static private boolean hasLibs() {
274 if (check_j3d) {
275 check_j3d = false;
276 try {
277 Class p3f = Class.forName("javax.vecmath.Point3f");
278 has_j3d_3dviewer = true;
279 } catch (ClassNotFoundException cnfe) {
280 Utils.log("Java 3D not installed.");
281 has_j3d_3dviewer = false;
282 return false;
284 try {
285 Class ij3d = Class.forName("ij3d.ImageWindow3D");
286 has_j3d_3dviewer = true;
287 } catch (ClassNotFoundException cnfe) {
288 Utils.log("3D Viewer not installed.");
289 has_j3d_3dviewer = false;
290 return false;
293 return has_j3d_3dviewer;
296 /** Get an existing Display3D for the given LayerSet, or create a new one for it (and cache it). */
297 static private Display3D get(final LayerSet ls) {
298 synchronized (htlock) {
299 htlock.lock();
300 try {
301 // test:
302 if (!hasLibs()) return null;
304 Object ob = ht_layer_sets.get(ls);
305 if (null == ob) {
306 final boolean[] done = new boolean[]{false};
307 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() {
308 Display3D ob = new Display3D(ls);
309 ht_layer_sets.put(ls, ob);
310 done[0] = true;
311 }});
312 // wait to avoid crashes in amd64
313 // try { Thread.sleep(500); } catch (Exception e) {}
314 while (!done[0]) {
315 try { Thread.sleep(50); } catch (Exception e) {}
317 ob = ht_layer_sets.get(ls);
319 return (Display3D)ob;
320 } catch (Exception e) {
321 IJError.print(e);
322 } finally {
323 // executed even when returning from within the try-catch block
324 htlock.unlock();
327 return null;
330 /** Get the Display3D instance that exists for the given LayerSet, if any. */
331 static public Display3D getDisplay(final LayerSet ls) {
332 return (Display3D)ht_layer_sets.get(ls);
335 static public void setWaitingCursor() {
336 for (Iterator it = ht_layer_sets.values().iterator(); it.hasNext(); ) {
337 ((Display3D)it.next()).universe.getWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
341 static public void doneWaiting() {
342 for (Iterator it = ht_layer_sets.values().iterator(); it.hasNext(); ) {
343 ((Display3D)it.next()).universe.getWindow().setCursor(Cursor.getDefaultCursor());
347 static public void show(ProjectThing pt) {
348 show(pt, false, -1);
351 /** 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. */
352 static public Future<List<Content>> show(final ProjectThing pt, final boolean wait, final int resample) {
353 if (null == pt) return null;
354 Callable<List<Content>> c = new Callable<List<Content>>() {
355 public List<Content> call() {
356 try {
357 // scan the given ProjectThing for 3D-viewable items not present in the ht_meshes
358 // So: find arealist, pipe, ball, and profile_list types
359 HashSet hs = pt.findBasicTypeChildren();
360 if (null == hs || 0 == hs.size()) {
361 Utils.log("Node " + pt + " contains no 3D-displayable children");
362 return null;
365 final List<Content> list = new ArrayList<Content>();
367 for (Iterator it = hs.iterator(); it.hasNext(); ) {
368 // obtain the Displayable object under the node
369 ProjectThing child = (ProjectThing)it.next();
370 Object obc = child.getObject();
371 Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
372 if (null != displ) {
373 if (displ.getClass().equals(Profile.class)) {
374 //Utils.log("Display3D can't handle Bezier profiles at the moment.");
375 // handled by profile_list Thing
376 continue;
378 if (!displ.isVisible()) {
379 Utils.log("Skipping non-visible node " + displ);
380 continue;
383 //StopWatch sw = new StopWatch();
384 // obtain the containing LayerSet
385 Display3D d3d = null;
386 if (null != displ) d3d = Display3D.get(displ.getLayerSet());
387 else if (child.getType().equals("profile_list")) {
388 ArrayList al_children = child.getChildren();
389 if (null == al_children || 0 == al_children.size()) continue;
390 // else, get the first Profile and get its LayerSet
391 d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
392 } else {
393 Utils.log("Don't know what to do with node " + child);
395 if (null == d3d) {
396 Utils.log("Could not get a proper 3D display for node " + displ);
397 return null; // java3D not installed most likely
399 if (d3d.ht_pt_meshes.contains(child)) {
400 Utils.log2("Already here: " + child);
401 continue; // already here
403 setWaitingCursor(); // the above may be creating a display
404 //sw.elapsed("after creating and/or retrieving Display3D");
405 Future<Content> fu = d3d.addMesh(child, displ, resample);
406 if (wait && -1 != d3d.resample) {
407 Utils.log("joining...");
408 list.add(fu.get());
412 //sw.elapsed("after creating mesh");
415 return list;
417 } catch (Exception e) {
418 IJError.print(e);
419 return null;
420 } finally {
421 doneWaiting();
425 return launchers.submit(c);
428 static public void showOrthoslices(Patch p) {
429 Display3D d3d = get(p.getLayerSet());
430 d3d.adjustResampling();
431 //d3d.universe.resetView();
432 String title = makeTitle(p) + " orthoslices";
433 // remove if present
434 d3d.universe.removeContent(title);
435 PatchStack ps = p.makePatchStack();
436 ImagePlus imp = get8BitStack(ps);
437 d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
438 Content ct = d3d.universe.getContent(title);
439 setTransform(ct, ps.getPatch(0));
440 ct.toggleLock(); // locks the added content
443 static public void showVolume(Patch p) {
444 Display3D d3d = get(p.getLayerSet());
445 d3d.adjustResampling();
446 //d3d.universe.resetView();
447 String title = makeTitle(p) + " volume";
448 // remove if present
449 d3d.universe.removeContent(title);
450 PatchStack ps = p.makePatchStack();
451 ImagePlus imp = get8BitStack(ps);
452 d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
453 Content ct = d3d.universe.getContent(title);
454 setTransform(ct, ps.getPatch(0));
455 ct.toggleLock(); // locks the added content
458 static private void setTransform(Content ct, Patch p) {
459 final double[] a = new double[6];
460 p.getAffineTransform().getMatrix(a);
461 Calibration cal = p.getLayerSet().getCalibration();
462 // a is: m00 m10 m01 m11 m02 m12
463 // d expects: m01 m02 m03 m04, m11 m12 ...
464 ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
465 a[1], a[3], 0, a[5] * cal.pixelWidth,
466 0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
467 0, 0, 0, 1}));
470 /** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
471 * If the PatchStach is already of the right type, it is returned,
472 * otherwise a copy is made in the proper type.
474 static private ImagePlus get8BitStack(final PatchStack ps) {
475 switch (ps.getType()) {
476 case ImagePlus.COLOR_RGB:
477 // convert stack to 8-bit color
478 return ps.createColor256Copy();
479 case ImagePlus.GRAY16:
480 case ImagePlus.GRAY32:
481 // convert stack to 8-bit
482 return ps.createGray8Copy();
483 default:
484 return ps;
488 /** A Material, but avoiding name colisions. */
489 static private int mat_index = 1;
490 static private class Mtl {
491 float alpha = 1;
492 float R = 1;
493 float G = 1;
494 float B = 1;
495 String name;
496 Mtl(float alpha, float R, float G, float B) {
497 this.alpha = alpha;
498 this.R = R;
499 this.G = G;
500 this.B = B;
501 name = "mat_" + mat_index;
502 mat_index++;
504 public boolean equals(Object ob) {
505 if (ob instanceof Display3D.Mtl) {
506 Mtl mat = (Mtl)ob;
507 if (mat.alpha == alpha
508 && mat.R == R
509 && mat.G == G
510 && mat.B == B) {
511 return true;
514 return false;
516 void fill(StringBuffer sb) {
517 sb.append("\nnewmtl ").append(name).append('\n')
518 .append("Ns 96.078431\n")
519 .append("Ka 0.0 0.0 0.0\n")
520 .append("Kd ").append(R).append(' ').append(G).append(' ').append(B).append('\n') // this is INCORRECT but I'll figure out the conversion later
521 .append("Ks 0.5 0.5 0.5\n")
522 .append("Ni 1.0\n")
523 .append("d ").append(alpha).append('\n')
524 .append("illum 2\n\n");
526 int getAsSingle() {
527 return (int)((R + G + B) / 3 * 255); // something silly
531 /** Generates DXF file from a table of ProjectThing and their associated triangles. */
532 private String createDXF(Hashtable ht_content) {
533 StringBuffer sb_data = new StringBuffer("0\nSECTION\n2\nENTITIES\n"); //header of file
534 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
535 Map.Entry entry = (Map.Entry)it.next();
536 ProjectThing pt = (ProjectThing)entry.getKey();
537 Displayable displ = (Displayable)pt.getObject();
538 List triangles = (List)entry.getValue();
539 float[] color = displ.getColor().getColorComponents(null);
540 Mtl mtl = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
541 writeTrianglesDXF(sb_data, triangles, mtl.name, Integer.toString(mtl.getAsSingle()));
543 sb_data.append("0\nENDSEC\n0\nEOF\n"); //TRAILER of the file
544 return sb_data.toString();
547 /** @param format works as extension as well. */
548 private void export(final ProjectThing pt, final String format) {
549 if (0 == ht_pt_meshes.size()) return;
550 // select file
551 File file = Utils.chooseFile("untitled", format);
552 if (null == file) return;
553 final String name = file.getName();
554 String name2 = name;
555 if (!name2.endsWith("." + format)) {
556 name2 += "." + format;
558 File f2 = new File(file.getParent() + "/" + name2);
559 int i = 1;
560 while (f2.exists()) {
561 name2 = name + "_" + i + "." + format;
562 f2 = new File(name2);
564 Hashtable ht_content = ht_pt_meshes;
565 if (null != pt) {
566 ht_content = new Hashtable();
567 ht_content.put(pt, ht_pt_meshes.get(pt));
569 if (format.equals("obj")) {
570 String[] data = createObjAndMtl(name2, ht_content);
571 Utils.saveToFile(f2, data[0]);
572 Utils.saveToFile(new File(f2.getParent() + "/" + name2 + ".mtl"), data[1]);
573 } else if (format.equals("dxf")) {
574 Utils.saveToFile(f2, createDXF(ht_content));
578 /** Wavefront format. Returns the contents of two files: one for materials, another for meshes*/
579 private String[] createObjAndMtl(final String file_name, final Hashtable ht_content) {
580 StringBuffer sb_obj = new StringBuffer("# TrakEM2 OBJ File\n");
581 sb_obj.append("mtllib ").append(file_name).append(".mtl").append('\n');
583 Hashtable ht_mat = new Hashtable();
585 int j = 1; // Vert indices in .obj files are global, not reset for every object.
586 // starting at '1' because vert indices start at one.
588 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
589 Map.Entry entry = (Map.Entry)it.next(); // I hate java's gratuituous verbosity
590 ProjectThing pt = (ProjectThing)entry.getKey();
591 Displayable displ = (Displayable)pt.getObject();
592 List triangles = (List)entry.getValue();
593 // make material, and see whether it exists already
594 float[] color = displ.getColor().getColorComponents(null);
595 Mtl mat = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
596 Object mat2 = ht_mat.get(mat);
597 if (null != mat2) mat = (Mtl)mat2; // recycling
598 else ht_mat.put(mat, mat); // !@#$% Can't get the object in a HashSet easily
599 // make list of vertices
600 String title = displ.getProject().getMeaningfulTitle(displ).replaceAll(" ", "_").replaceAll("#", "--");
601 Hashtable ht_points = new Hashtable(); // because we like inefficiency
602 sb_obj.append("o ").append(title).append('\n');
603 final int len = triangles.size();
604 int[] index = new int[len];
605 int k = 0; // iterate over index array, to make faces later
606 // j is tag for each new vert, which start at 1 (for some ridiculous reason)
607 for (Iterator tt = triangles.iterator(); tt.hasNext(); ) {
608 Point3f p = (Point3f)tt.next();
609 //no need if coords are not displaced//p = (Point3f)p.clone();
610 // check if point already exists
611 Object ob = ht_points.get(p);
612 if (null != ob) {
613 index[k] = ((Integer)ob).intValue();
614 } else {
615 // new point
616 index[k] = j;
617 // record
618 ht_points.put(p, new Integer(j));
619 // append vertex
620 sb_obj.append('v').append(' ').append(p.x)
621 .append(' ').append(p.y)
622 .append(' ').append(p.z).append('\n');
623 j++;
625 k++;
627 sb_obj.append("usemtl ").append(mat.name).append('\n');
628 sb_obj.append("s 1\n");
629 if (0 != len % 3) Utils.log2("WARNING: list of triangles not multiple of 3");
630 // print faces
631 int len_p = ht_points.size();
632 for (int i=0; i<len; i+=3) {
633 sb_obj.append('f').append(' ').append(index[i])
634 .append(' ').append(index[i+1])
635 .append(' ').append(index[i+2]).append('\n');
636 //if (index[i] > len_p) Utils.log2("WARNING: face vert index beyond range"); // range is from 1 to len_p inclusive
637 //if (index[i+1] > len_p) Utils.log2("WARNING: face vert index beyond range");
638 //if (index[i+2] > len_p) Utils.log2("WARNING: face vert index beyond range");
639 //Utils.log2("j: " + index[i]);
640 // checks passed
642 sb_obj.append('\n');
644 // make mtl file
645 StringBuffer sb_mtl = new StringBuffer("# TrakEM2 MTL File\n");
646 for (Iterator it = ht_mat.keySet().iterator(); it.hasNext(); ) {
647 Mtl mat = (Mtl)it.next();
648 mat.fill(sb_mtl);
651 return new String[]{sb_obj.toString(), sb_mtl.toString()};
654 /** Considers there is only one Display3D for each LayerSet. */
655 static public void remove(ProjectThing pt) {
656 if (null == pt) return;
657 if (null == pt.getObject()) return;
658 Object ob = pt.getObject();
659 if (!(ob instanceof Displayable)) return;
660 Displayable displ = (Displayable)ob;
661 Object d3ob = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
662 if (null == d3ob) {
663 // there is no Display3D showing the pt to remove
664 Utils.log2("No Display3D contains ProjectThing: " + pt);
665 return;
667 Display3D d3d = (Display3D)d3ob;
668 Object ob_mesh = d3d.ht_pt_meshes.remove(pt);
669 if (null == ob_mesh) {
670 Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
671 return; // not contained here
673 String title = makeTitle(displ);
674 //Utils.log(d3d.universe.contains(title) + ": Universe contains " + displ);
675 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.
678 static private void writeTrianglesDXF(final StringBuffer sb, final List triangles, final String the_group, final String the_color) {
680 final char L = '\n';
681 final String s10 = "10\n"; final String s11 = "11\n"; final String s12 = "12\n"; final String s13 = "13\n";
682 final String s20 = "20\n"; final String s21 = "21\n"; final String s22 = "22\n"; final String s23 = "23\n";
683 final String s30 = "30\n"; final String s31 = "31\n"; final String s32 = "32\n"; final String s33 = "33\n";
684 final String triangle_header = "0\n3DFACE\n8\n" + the_group + "\n6\nCONTINUOUS\n62\n" + the_color + L;
686 final int len = triangles.size();
687 final Point3f[] vert = new Point3f[len];
688 triangles.toArray(vert);
689 for (int i=0; i<len; i+=3) {
691 sb.append(triangle_header)
693 .append(s10).append(vert[i].x).append(L)
694 .append(s20).append(vert[i].y).append(L)
695 .append(s30).append(vert[i].z).append(L)
697 .append(s11).append(vert[i+1].x).append(L)
698 .append(s21).append(vert[i+1].y).append(L)
699 .append(s31).append(vert[i+1].z).append(L)
701 .append(s12).append(vert[i+2].x).append(L)
702 .append(s22).append(vert[i+2].y).append(L)
703 .append(s32).append(vert[i+2].z).append(L)
705 .append(s13).append(vert[i+2].x).append(L) // repeated point
706 .append(s23).append(vert[i+2].y).append(L)
707 .append(s33).append(vert[i+2].z).append(L);
711 /** Creates a mesh for the given Displayable in a separate Thread. */
712 private Future<Content> addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
713 final double scale = this.scale;
714 FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
715 public Content call() {
716 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
717 try {
719 // 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.)
720 List triangles = null;
721 boolean no_culling = false; // don't show back faces when false
722 if (displ instanceof AreaList) {
723 int rs = resample;
724 if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
725 else rs = Display3D.this.resample;
726 triangles = ((AreaList)displ).generateTriangles(scale, rs);
727 //triangles = removeNonManifold(triangles);
728 } else if (displ instanceof Ball) {
729 double[][][] globe = Ball.generateGlobe(12, 12);
730 triangles = ((Ball)displ).generateTriangles(scale, globe);
731 } else if (displ instanceof Line3D) {
732 // Pipe and Polyline
733 // adjustResampling(); // fails horribly, needs first to correct mesh-generation code
734 triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
735 } else if (null == displ && pt.getType().equals("profile_list")) {
736 triangles = Profile.generateTriangles(pt, scale);
737 no_culling = true;
739 // safety checks
740 if (null == triangles) {
741 Utils.log("Some error ocurred: can't create triangles for " + displ);
742 return null;
744 if (0 == triangles.size()) {
745 Utils.log2("Skipping empty mesh for " + displ.getTitle());
746 return null;
748 if (0 != triangles.size() % 3) {
749 Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
750 return null;
752 Color color = null;
753 float alpha = 1.0f;
754 if (null != displ) {
755 color = displ.getColor();
756 alpha = displ.getAlpha();
757 } else {
758 // for profile_list: get from the first (what a kludge)
759 Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
760 if (null == obp) return null;
761 Displayable di = (Displayable)obp;
762 color = di.getColor();
763 alpha = di.getAlpha();
766 Content ct = null;
768 // add to 3D view (synchronized)
769 synchronized (u_lock) {
770 u_lock.lock();
771 try {
772 // craft a unique title (id is always unique)
773 String title = null == displ ? pt.toString() + " #" + pt.getId() : makeTitle(displ);
774 if (ht_pt_meshes.contains(pt) || universe.contains(title)) {
775 // remove content from universe
776 universe.removeContent(title);
777 // no need to remove entry from table, it's overwritten below
779 // register mesh
780 ht_pt_meshes.put(pt, triangles);
781 // ensure proper default transform
782 //universe.resetView();
784 Color3f c3 = new Color3f(color);
786 if (no_culling) {
787 // create a mesh with the same color and zero transparency (that is, full opacity)
788 CustomTriangleMesh mesh = new CustomTriangleMesh(triangles, c3, 0);
789 // Set mesh properties for double-sided triangles
790 PolygonAttributes pa = mesh.getAppearance().getPolygonAttributes();
791 pa.setCullFace(PolygonAttributes.CULL_NONE);
792 pa.setBackFaceNormalFlip(true);
793 // After setting properties, add to the viewer
794 ct = universe.addCustomMesh(mesh, c3, title);
795 } else {
796 ct = universe.addTriangleMesh(triangles, new Color3f(color), title);
799 if (null == ct) return null;
801 // Set general content properties
802 ct.setTransparency(1f - alpha);
803 // Default is unlocked (editable) transformation; set it to locked:
804 ct.toggleLock();
806 } catch (Exception e) {
807 IJError.print(e);
809 u_lock.unlock();
812 Utils.log2(pt.toString() + " n points: " + triangles.size());
814 return ct;
816 } catch (Exception e) {
817 IJError.print(e);
818 return null;
821 }});
822 executors.execute(fu);
823 return fu;
826 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
827 static public Future<Content> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
828 return addMesh(ref_ls, vs, title, color, null, 1.0f);
831 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
832 static public Future<Content> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha) {
833 final FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
834 public Content call() {
835 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
836 try {
837 /////
838 final Display3D d3d = Display3D.get(ref_ls);
839 final double scale = d3d.scale;
840 final double width = d3d.width;
841 float transp = 1 - alpha;
842 if (transp < 0) transp = 0;
843 if (transp > 1) transp = 1;
844 if (1 == transp) {
845 Utils.log("WARNING: adding a 3D object fully transparent.");
848 double[] wi = widths;
849 if (null == widths) {
850 wi = new double[vs.getPoints(0).length];
851 //Utils.log2("len: " + wi.length + vs.getPoints(0).length + vs.getPoints(1).length);
852 Arrays.fill(wi, 2.0);
853 } else if (widths.length != vs.length()) {
854 Utils.log("ERROR: widths.length != VectorString3D.length()");
855 return null;
858 List triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), scale);
860 Content ct = null;
862 // add to 3D view (synchronized)
863 synchronized (d3d.u_lock) {
864 d3d.u_lock.lock();
865 try {
866 // ensure proper default transform
867 //d3d.universe.resetView();
869 //Utils.log2(title + " : vertex count % 3 = " + triangles.size() % 3 + " for " + triangles.size() + " vertices");
870 //d3d.universe.ensureScale((float)(width*scale));
871 ct = d3d.universe.addMesh(triangles, new Color3f(color), title, /*(float)(width*scale),*/ 1);
872 ct.setTransparency(transp);
873 ct.toggleLock();
874 } catch (Exception e) {
875 IJError.print(e);
877 d3d.u_lock.unlock();
880 return ct;
882 /////
883 } catch (Exception e) {
884 IJError.print(e);
885 return null;
888 }});
891 launchers.execute(new Runnable() { public void run() {
892 final Display3D d3d = Display3D.get(ref_ls);
893 d3d.executors.execute(fu);
894 }});
896 return fu;
899 // This method has the exclusivity in adjusting the resampling value.
900 synchronized private final int adjustResampling() {
901 if (resample > 0) return resample;
902 final GenericDialog gd = new GenericDialog("Resample");
903 gd.addSlider("Resample: ", 1, 20, -1 != resample ? resample : DEFAULT_RESAMPLE);
904 gd.showDialog();
905 if (gd.wasCanceled()) {
906 resample = -1 != resample ? resample : DEFAULT_RESAMPLE; // current or default value
907 return resample;
909 resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
910 return resample;
913 /** Checks if there is any Display3D instance currently showing the given Displayable. */
914 static public boolean isDisplayed(final Displayable d) {
915 if (null == d) return false;
916 final String title = makeTitle(d);
917 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
918 Display3D d3d = (Display3D)it.next();
919 if (null != d3d.universe.getContent(title)) return true;
921 if (d.getClass() == Profile.class) {
922 Content content = getProfileContent(d);
924 return false;
927 /** 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. */
928 static public Content getProfileContent(final Displayable d) {
929 if (null == d) return null;
930 if (d.getClass() != Profile.class) return null;
931 Display3D d3d = get(d.getLayer().getParent());
932 if (null == d3d) return null;
933 ProjectThing pt = d.getProject().findProjectThing(d);
934 if (null == pt) return null;
935 pt = (ProjectThing) pt.getParent();
936 return d3d.universe.getContent(new StringBuffer(pt.toString()).append(" #").append(pt.getId()).toString());
939 static public void setColor(final Displayable d, final Color color) {
940 launchers.execute(new Runnable() { public void run() {
941 final Display3D d3d = get(d.getLayer().getParent());
942 if (null == d3d) return; // no 3D displays open
943 d3d.executors.execute(new Runnable() { public void run() {
944 Content content = d3d.universe.getContent(makeTitle(d));
945 if (null == content) content = getProfileContent(d);
946 if (null != content) content.setColor(new Color3f(color));
947 }});
948 }});
951 static public void setTransparency(final Displayable d, final float alpha) {
952 if (null == d) return;
953 Layer layer = d.getLayer();
954 if (null == layer) return; // some objects have no layer, such as the parent LayerSet.
955 Object ob = ht_layer_sets.get(layer.getParent());
956 if (null == ob) return;
957 Display3D d3d = (Display3D)ob;
958 String title = makeTitle(d);
959 Content content = d3d.universe.getContent(title);
960 if (null == content) content = getProfileContent(d);
961 if (null != content) content.setTransparency(1 - alpha);
962 else if (null == content && d.getClass().equals(Patch.class)) {
963 Patch pa = (Patch)d;
964 if (pa.isStack()) {
965 title = pa.getProject().getLoader().getFileName(pa);
966 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
967 d3d = (Display3D)it.next();
968 for (Iterator cit = d3d.universe.getContents().iterator(); cit.hasNext(); ) {
969 Content c = (Content)cit.next();
970 if (c.getName().startsWith(title)) {
971 c.setTransparency(1 - alpha);
972 // no break, since there could be a volume and an orthoslice
980 static public String makeTitle(final Displayable d) {
981 return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
983 static public String makeTitle(final Patch p) {
984 return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
985 + " #" + p.getProject().getLoader().getNextId();
988 /** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
989 * (otherwise returns null). */
990 static public Future<Content> update(final Displayable d) {
991 Layer layer = d.getLayer();
992 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
993 Object ob = ht_layer_sets.get(layer.getParent());
994 if (null == ob) return null;
995 Display3D d3d = (Display3D)ob;
996 return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
1000 static public final double computeTriangleArea() {
1001 return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
1002 Math.pow(yA*zB + yB*zC + yC*zA, 2) +
1003 Math.pow(zA*xB + zB*xC + zC*xA, 2));
1007 static public final boolean contains(final LayerSet ls, final String title) {
1008 final Display3D d3d = getDisplay(ls);
1009 if (null == d3d) return false;
1010 return null != d3d.universe.getContent(title);
1013 static public void destroy() {
1014 launchers.shutdownNow();