Moved Display3D to new 3D Viewer API.
[trakem2.git] / ini / trakem2 / display / Display3D.java
blob4fa0a79aadc8a4d0901e7fc05b7b5caa025e142e
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.event.ActionListener;
26 import java.awt.event.ItemListener;
27 import java.awt.event.ActionEvent;
28 import java.awt.event.ItemEvent;
29 import java.util.*;
30 import java.io.File;
31 import java.awt.geom.AffineTransform;
33 import java.awt.event.WindowAdapter;
34 import java.awt.event.WindowEvent;
36 import javax.vecmath.Point3f;
37 import javax.vecmath.Color3f;
38 import javax.vecmath.Vector3f;
39 import javax.media.j3d.View;
40 import javax.media.j3d.Transform3D;
42 import ij3d.ImageWindow3D;
43 import ij3d.Image3DUniverse;
44 import ij3d.Content;
45 import ij3d.Image3DMenubar;
47 import java.lang.reflect.Field;
50 /** One Display3D instance for each LayerSet (maximum). */
51 public final class Display3D {
53 /** The threading is so poorly done ... it BARELY works, it's fragile. */
55 /** Table of LayerSet and Display3D - since there is a one to one relationship. */
56 static private Hashtable ht_layer_sets = new Hashtable();
57 /**Control calls to new Display3D. */
58 static private Lock htlock = new Lock();
60 /** 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. */
61 static public Hashtable getMasterTable() {
62 return (Hashtable)ht_layer_sets.clone();
65 /** Table of ProjectThing keys versus meshes, the latter represented by List of triangles in the form of thre econsecutive Point3f in the List.*/
66 private Hashtable ht_pt_meshes = new Hashtable();
68 private Image3DUniverse universe;
70 private Lock u_lock = new Lock();
72 private LayerSet layer_set;
73 private double width, height;
74 private int resample = -1; // unset
75 static private final int DEFAULT_RESAMPLE = 4;
76 /** If the LayerSet dimensions are too large, then limit to max 2048 for width or height and setup a scale.*/
77 private double scale = 1.0;
78 static private final int MAX_DIMENSION = 1024;
80 private String selected = null;
82 /** Defaults to parallel projection. */
83 private Display3D(final LayerSet ls) {
84 this.layer_set = ls;
85 this.universe = new Image3DUniverse(512, 512); // size of the initial canvas, not the universe itself
86 this.universe.getViewer().getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); // (View.PERSPECTIVE_PROJECTION);
87 computeScale(ls);
88 this.universe.show();
89 this.universe.getWindow().addWindowListener(new IW3DListener(ls));
91 // register
92 Display3D.ht_layer_sets.put(ls, this);
95 public Image3DUniverse getUniverse() {
96 return universe;
99 /* Take a snapshot know-it-all mode. Each Transform3D given as argument gets assigned to the (nearly) homonimous TransformGroup, which have the following relationships:
101 * scaleTG contains rotationsTG
102 * rotationsTG contains translateTG
103 * translateTG contains centerTG
104 * centerTG contains the whole scene, with all meshes, etc.
106 * Any null arguments imply the current transform in the open Display3D.
108 * 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.
110 * The TransformGroup instances may be reached like this:
112 * LayerSet layer_set = Display.getFrontLayer().getParent();
113 * Display3D d3d = Display3D.getDisplay(layer_set);
114 * TransformGroup scaleTG = d3d.getUniverse().getGlobalScale();
115 * TransformGroup rotationsTG = d3d.getUniverse().getGlobalRotate();
116 * TransformGroup translateTG = d3d.getUniverse().getGlobalTranslate();
117 * TransformGroup centerTG = d3d.getUniverse().getCenterTG();
119 * ... and the Transform3D from each may be read out indirectly like this:
121 * Transform3D t_scale = new Transform3D();
122 * scaleTG.getTransform(t_scale);
123 * ...
125 * 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).
128 /*public ImagePlus makeSnapshot(final Transform3D scale, final Transform3D rotate, final Transform3D translate, final Transform3D center) {
129 return universe.makeSnapshot(scale, rotate, translate, center);
132 /** Uses current scaling, translation and centering transforms! */
133 /*public ImagePlus makeSnapshotXY() { // aka posterior
134 // default view
135 return universe.makeSnapshot(null, new Transform3D(), null, null);
137 /** Uses current scaling, translation and centering transforms! */
138 /*public ImagePlus makeSnapshotXZ() { // aka dorsal
139 Transform3D rot1 = new Transform3D();
140 rot1.rotZ(-Math.PI/2);
141 Transform3D rot2 = new Transform3D();
142 rot2.rotX(Math.PI/2);
143 rot1.mul(rot2);
144 return universe.makeSnapshot(null, rot1, null, null);
147 /** Uses current scaling, translation and centering transforms! */
149 public ImagePlus makeSnapshotYZ() { // aka lateral
150 Transform3D rot = new Transform3D();
151 rot.rotY(Math.PI/2);
152 return universe.makeSnapshot(null, rot, null, null);
156 public ImagePlus makeSnapshotZX() { // aka frontal
157 Transform3D rot = new Transform3D();
158 rot.rotX(-Math.PI/2);
159 return universe.makeSnapshot(null, rot, null, null);
163 /** Uses current scaling, translation and centering transforms! Opposite side of XZ. */
165 public ImagePlus makeSnapshotXZOpp() {
166 Transform3D rot1 = new Transform3D();
167 rot1.rotX(-Math.PI/2); // 90 degrees clockwise
168 Transform3D rot2 = new Transform3D();
169 rot2.rotY(Math.PI); // 180 degrees around Y, to the other side.
170 rot1.mul(rot2);
171 return universe.makeSnapshot(null, rot1, null, null);
174 private class IW3DListener extends WindowAdapter {
175 private LayerSet ls;
176 IW3DListener(LayerSet ls) {
177 this.ls = ls;
179 public void windowClosing(WindowEvent we) {
180 Utils.log2("Display3D.windowClosing");
181 /*Object ob =*/ ht_layer_sets.remove(ls);
182 /*if (null != ob) {
183 Utils.log2("Removed Display3D from table for LayerSet " + ls);
186 public void windowClosed(WindowEvent we) {
187 Utils.log2("Display3D.windowClosed");
188 ht_layer_sets.remove(ls);
192 /** Reads the #ID in the name, which is immutable. */
193 private ProjectThing find(String name) {
194 long id = Long.parseLong(name.substring(name.lastIndexOf('#')+1));
195 for (Iterator it = ht_pt_meshes.keySet().iterator(); it.hasNext(); ) {
196 ProjectThing pt = (ProjectThing)it.next();
197 Displayable d = (Displayable)pt.getObject();
198 if (d.getId() == id) {
199 return pt;
202 return null;
205 /** 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. */
206 private void computeScale(LayerSet ls) {
207 this.width = ls.getLayerWidth();
208 this.height = ls.getLayerHeight();
209 if (width > MAX_DIMENSION) {
210 scale = MAX_DIMENSION / width;
211 height *= scale;
212 width = MAX_DIMENSION;
214 if (height > MAX_DIMENSION) {
215 scale = MAX_DIMENSION / height;
216 width *= scale;
217 height = MAX_DIMENSION;
219 //Utils.log2("scale, width, height: " + scale + ", " + width + ", " + height);
222 static private boolean check_j3d = true;
223 static private boolean has_j3d_3dviewer = false;
225 static private boolean hasLibs() {
226 if (check_j3d) {
227 check_j3d = false;
228 try {
229 Class p3f = Class.forName("javax.vecmath.Point3f");
230 has_j3d_3dviewer = true;
231 } catch (ClassNotFoundException cnfe) {
232 Utils.log("Java 3D not installed.");
233 has_j3d_3dviewer = false;
234 return false;
236 try {
237 Class ij3d = Class.forName("ij3d.ImageWindow3D");
238 has_j3d_3dviewer = true;
239 } catch (ClassNotFoundException cnfe) {
240 Utils.log("3D Viewer not installed.");
241 has_j3d_3dviewer = false;
242 return false;
245 return has_j3d_3dviewer;
248 /** Get an existing Display3D for the given LayerSet, or create a new one for it (and cache it). */
249 static private Display3D get(final LayerSet ls) {
250 synchronized (htlock) {
251 htlock.lock();
252 try {
253 // test:
254 if (!hasLibs()) return null;
256 Object ob = ht_layer_sets.get(ls);
257 if (null == ob) {
258 final boolean[] done = new boolean[]{false};
259 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() {
260 Display3D ob = new Display3D(ls);
261 ht_layer_sets.put(ls, ob);
262 done[0] = true;
263 }});
264 // wait to avoid crashes in amd64
265 // try { Thread.sleep(500); } catch (Exception e) {}
266 while (!done[0]) {
267 try { Thread.sleep(50); } catch (Exception e) {}
269 ob = ht_layer_sets.get(ls);
271 return (Display3D)ob;
272 } catch (Exception e) {
273 IJError.print(e);
274 } finally {
275 // executed even when returning from within the try-catch block
276 htlock.unlock();
279 return null;
282 /** Get the Display3D instance that exists for the given LayerSet, if any. */
283 static public Display3D getDisplay(final LayerSet ls) {
284 return (Display3D)ht_layer_sets.get(ls);
287 static public void setWaitingCursor() {
288 for (Iterator it = ht_layer_sets.values().iterator(); it.hasNext(); ) {
289 ((Display3D)it.next()).universe.getWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
293 static public void doneWaiting() {
294 for (Iterator it = ht_layer_sets.values().iterator(); it.hasNext(); ) {
295 ((Display3D)it.next()).universe.getWindow().setCursor(Cursor.getDefaultCursor());
299 static public void show(ProjectThing pt) {
300 show(pt, false, -1);
303 /** 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. */
304 static public void show(ProjectThing pt, boolean wait, int resample) {
305 if (null == pt) return;
306 try {
307 // scan the given ProjectThing for 3D-viewable items not present in the ht_meshes
308 // So: find arealist, pipe, ball, and profile_list types
309 HashSet hs = pt.findBasicTypeChildren();
310 if (null == hs || 0 == hs.size()) {
311 Utils.log("Node " + pt + " contains no 3D-displayable children");
312 return;
315 for (Iterator it = hs.iterator(); it.hasNext(); ) {
316 // obtain the Displayable object under the node
317 ProjectThing child = (ProjectThing)it.next();
318 Object obc = child.getObject();
319 Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
320 if (null != displ) {
321 if (displ.getClass().equals(Profile.class)) {
322 //Utils.log("Display3D can't handle Bezier profiles at the moment.");
323 // handled by profile_list Thing
324 continue;
326 if (!displ.isVisible()) {
327 Utils.log("Skipping non-visible node " + displ);
328 continue;
331 //StopWatch sw = new StopWatch();
332 // obtain the containing LayerSet
333 Display3D d3d = null;
334 if (null != displ) d3d = Display3D.get(displ.getLayerSet());
335 else if (child.getType().equals("profile_list")) {
336 ArrayList al_children = child.getChildren();
337 if (null == al_children || 0 == al_children.size()) continue;
338 // else, get the first Profile and get its LayerSet
339 d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
340 } else {
341 Utils.log("Don't know what to do with node " + child);
343 if (null == d3d) {
344 Utils.log("Could not get a proper 3D display for node " + displ);
345 return; // java3D not installed most likely
347 if (d3d.ht_pt_meshes.contains(child)) {
348 Utils.log2("Already here: " + child);
349 continue; // already here
351 setWaitingCursor(); // the above may be creating a display
352 //sw.elapsed("after creating and/or retrieving Display3D");
353 Thread t = d3d.addMesh(child, displ, resample);
354 if (wait && -1 != d3d.resample) {
355 Utils.log("joining...");
356 try { t.join(); } catch (Exception e) { e.printStackTrace(); }
358 //sw.elapsed("after creating mesh");
360 } catch (Exception e) {
361 IJError.print(e);
362 } finally {
363 doneWaiting();
367 static public void showOrthoslices(Patch p) {
368 Display3D d3d = get(p.getLayerSet());
369 d3d.adjustResampling();
370 //d3d.universe.resetView();
371 String title = makeTitle(p) + " orthoslices";
372 // remove if present
373 d3d.universe.removeContent(title);
374 PatchStack ps = p.makePatchStack();
375 ImagePlus imp = get8BitStack(ps);
376 d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
377 Content ct = d3d.universe.getContent(title);
378 setTransform(ct, ps.getPatch(0));
379 ct.toggleLock(); // locks the added content
382 static public void showVolume(Patch p) {
383 Display3D d3d = get(p.getLayerSet());
384 d3d.adjustResampling();
385 //d3d.universe.resetView();
386 String title = makeTitle(p) + " volume";
387 // remove if present
388 d3d.universe.removeContent(title);
389 PatchStack ps = p.makePatchStack();
390 ImagePlus imp = get8BitStack(ps);
391 d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
392 Content ct = d3d.universe.getContent(title);
393 setTransform(ct, ps.getPatch(0));
394 ct.toggleLock(); // locks the added content
397 static private void setTransform(Content ct, Patch p) {
398 final double[] a = new double[6];
399 p.getAffineTransform().getMatrix(a);
400 Calibration cal = p.getLayerSet().getCalibration();
401 // a is: m00 m10 m01 m11 m02 m12
402 // d expects: m01 m02 m03 m04, m11 m12 ...
403 ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
404 a[1], a[3], 0, a[5] * cal.pixelWidth,
405 0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
406 0, 0, 0, 1}));
409 /** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
410 * If the PatchStach is already of the right type, it is returned,
411 * otherwise a copy is made in the proper type.
413 static private ImagePlus get8BitStack(final PatchStack ps) {
414 switch (ps.getType()) {
415 case ImagePlus.COLOR_RGB:
416 // convert stack to 8-bit color
417 return ps.createColor256Copy();
418 case ImagePlus.GRAY16:
419 case ImagePlus.GRAY32:
420 // convert stack to 8-bit
421 return ps.createGray8Copy();
422 default:
423 return ps;
427 /** A Material, but avoiding name colisions. */
428 static private int mat_index = 1;
429 static private class Mtl {
430 float alpha = 1;
431 float R = 1;
432 float G = 1;
433 float B = 1;
434 String name;
435 Mtl(float alpha, float R, float G, float B) {
436 this.alpha = alpha;
437 this.R = R;
438 this.G = G;
439 this.B = B;
440 name = "mat_" + mat_index;
441 mat_index++;
443 public boolean equals(Object ob) {
444 if (ob instanceof Display3D.Mtl) {
445 Mtl mat = (Mtl)ob;
446 if (mat.alpha == alpha
447 && mat.R == R
448 && mat.G == G
449 && mat.B == B) {
450 return true;
453 return false;
455 void fill(StringBuffer sb) {
456 sb.append("\nnewmtl ").append(name).append('\n')
457 .append("Ns 96.078431\n")
458 .append("Ka 0.0 0.0 0.0\n")
459 .append("Kd ").append(R).append(' ').append(G).append(' ').append(B).append('\n') // this is INCORRECT but I'll figure out the conversion later
460 .append("Ks 0.5 0.5 0.5\n")
461 .append("Ni 1.0\n")
462 .append("d ").append(alpha).append('\n')
463 .append("illum 2\n\n");
465 int getAsSingle() {
466 return (int)((R + G + B) / 3 * 255); // something silly
470 /** Generates DXF file from a table of ProjectThing and their associated triangles. */
471 private String createDXF(Hashtable ht_content) {
472 StringBuffer sb_data = new StringBuffer("0\nSECTION\n2\nENTITIES\n"); //header of file
473 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
474 Map.Entry entry = (Map.Entry)it.next();
475 ProjectThing pt = (ProjectThing)entry.getKey();
476 Displayable displ = (Displayable)pt.getObject();
477 List triangles = (List)entry.getValue();
478 float[] color = displ.getColor().getColorComponents(null);
479 Mtl mtl = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
480 writeTrianglesDXF(sb_data, triangles, mtl.name, Integer.toString(mtl.getAsSingle()));
482 sb_data.append("0\nENDSEC\n0\nEOF\n"); //TRAILER of the file
483 return sb_data.toString();
486 /** @param format works as extension as well. */
487 private void export(final ProjectThing pt, final String format) {
488 if (0 == ht_pt_meshes.size()) return;
489 // select file
490 File file = Utils.chooseFile("untitled", format);
491 if (null == file) return;
492 final String name = file.getName();
493 String name2 = name;
494 if (!name2.endsWith("." + format)) {
495 name2 += "." + format;
497 File f2 = new File(file.getParent() + "/" + name2);
498 int i = 1;
499 while (f2.exists()) {
500 name2 = name + "_" + i + "." + format;
501 f2 = new File(name2);
503 Hashtable ht_content = ht_pt_meshes;
504 if (null != pt) {
505 ht_content = new Hashtable();
506 ht_content.put(pt, ht_pt_meshes.get(pt));
508 if (format.equals("obj")) {
509 String[] data = createObjAndMtl(name2, ht_content);
510 Utils.saveToFile(f2, data[0]);
511 Utils.saveToFile(new File(f2.getParent() + "/" + name2 + ".mtl"), data[1]);
512 } else if (format.equals("dxf")) {
513 Utils.saveToFile(f2, createDXF(ht_content));
517 /** Wavefront format. Returns the contents of two files: one for materials, another for meshes*/
518 private String[] createObjAndMtl(final String file_name, final Hashtable ht_content) {
519 StringBuffer sb_obj = new StringBuffer("# TrakEM2 OBJ File\n");
520 sb_obj.append("mtllib ").append(file_name).append(".mtl").append('\n');
522 Hashtable ht_mat = new Hashtable();
524 int j = 1; // Vert indices in .obj files are global, not reset for every object.
525 // starting at '1' because vert indices start at one.
527 for (Iterator it = ht_content.entrySet().iterator(); it.hasNext(); ) {
528 Map.Entry entry = (Map.Entry)it.next(); // I hate java's gratuituous verbosity
529 ProjectThing pt = (ProjectThing)entry.getKey();
530 Displayable displ = (Displayable)pt.getObject();
531 List triangles = (List)entry.getValue();
532 // make material, and see whether it exists already
533 float[] color = displ.getColor().getColorComponents(null);
534 Mtl mat = new Mtl(displ.getAlpha(), color[0], color[1], color[2]);
535 Object mat2 = ht_mat.get(mat);
536 if (null != mat2) mat = (Mtl)mat2; // recycling
537 else ht_mat.put(mat, mat); // !@#$% Can't get the object in a HashSet easily
538 // make list of vertices
539 String title = displ.getProject().getMeaningfulTitle(displ).replaceAll(" ", "_").replaceAll("#", "--");
540 Hashtable ht_points = new Hashtable(); // because we like inefficiency
541 sb_obj.append("o ").append(title).append('\n');
542 final int len = triangles.size();
543 int[] index = new int[len];
544 int k = 0; // iterate over index array, to make faces later
545 // j is tag for each new vert, which start at 1 (for some ridiculous reason)
546 for (Iterator tt = triangles.iterator(); tt.hasNext(); ) {
547 Point3f p = (Point3f)tt.next();
548 //no need if coords are not displaced//p = (Point3f)p.clone();
549 // check if point already exists
550 Object ob = ht_points.get(p);
551 if (null != ob) {
552 index[k] = ((Integer)ob).intValue();
553 } else {
554 // new point
555 index[k] = j;
556 // record
557 ht_points.put(p, new Integer(j));
558 // append vertex
559 sb_obj.append('v').append(' ').append(p.x)
560 .append(' ').append(p.y)
561 .append(' ').append(p.z).append('\n');
562 j++;
564 k++;
566 sb_obj.append("usemtl ").append(mat.name).append('\n');
567 sb_obj.append("s 1\n");
568 if (0 != len % 3) Utils.log2("WARNING: list of triangles not multiple of 3");
569 // print faces
570 int len_p = ht_points.size();
571 for (int i=0; i<len; i+=3) {
572 sb_obj.append('f').append(' ').append(index[i])
573 .append(' ').append(index[i+1])
574 .append(' ').append(index[i+2]).append('\n');
575 //if (index[i] > len_p) Utils.log2("WARNING: face vert index beyond range"); // range is from 1 to len_p inclusive
576 //if (index[i+1] > len_p) Utils.log2("WARNING: face vert index beyond range");
577 //if (index[i+2] > len_p) Utils.log2("WARNING: face vert index beyond range");
578 //Utils.log2("j: " + index[i]);
579 // checks passed
581 sb_obj.append('\n');
583 // make mtl file
584 StringBuffer sb_mtl = new StringBuffer("# TrakEM2 MTL File\n");
585 for (Iterator it = ht_mat.keySet().iterator(); it.hasNext(); ) {
586 Mtl mat = (Mtl)it.next();
587 mat.fill(sb_mtl);
590 return new String[]{sb_obj.toString(), sb_mtl.toString()};
593 /** Considers there is only one Display3D for each LayerSet. */
594 static public void remove(ProjectThing pt) {
595 if (null == pt) return;
596 if (null == pt.getObject()) return;
597 Object ob = pt.getObject();
598 if (!(ob instanceof Displayable)) return;
599 Displayable displ = (Displayable)ob;
600 Object d3ob = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
601 if (null == d3ob) {
602 // there is no Display3D showing the pt to remove
603 Utils.log2("No Display3D contains ProjectThing: " + pt);
604 return;
606 Display3D d3d = (Display3D)d3ob;
607 Object ob_mesh = d3d.ht_pt_meshes.remove(pt);
608 if (null == ob_mesh) {
609 Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
610 return; // not contained here
612 String title = makeTitle(displ);
613 //Utils.log(d3d.universe.contains(title) + ": Universe contains " + displ);
614 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.
617 static private void writeTrianglesDXF(final StringBuffer sb, final List triangles, final String the_group, final String the_color) {
619 final char L = '\n';
620 final String s10 = "10\n"; final String s11 = "11\n"; final String s12 = "12\n"; final String s13 = "13\n";
621 final String s20 = "20\n"; final String s21 = "21\n"; final String s22 = "22\n"; final String s23 = "23\n";
622 final String s30 = "30\n"; final String s31 = "31\n"; final String s32 = "32\n"; final String s33 = "33\n";
623 final String triangle_header = "0\n3DFACE\n8\n" + the_group + "\n6\nCONTINUOUS\n62\n" + the_color + L;
625 final int len = triangles.size();
626 final Point3f[] vert = new Point3f[len];
627 triangles.toArray(vert);
628 for (int i=0; i<len; i+=3) {
630 sb.append(triangle_header)
632 .append(s10).append(vert[i].x).append(L)
633 .append(s20).append(vert[i].y).append(L)
634 .append(s30).append(vert[i].z).append(L)
636 .append(s11).append(vert[i+1].x).append(L)
637 .append(s21).append(vert[i+1].y).append(L)
638 .append(s31).append(vert[i+1].z).append(L)
640 .append(s12).append(vert[i+2].x).append(L)
641 .append(s22).append(vert[i+2].y).append(L)
642 .append(s32).append(vert[i+2].z).append(L)
644 .append(s13).append(vert[i+2].x).append(L) // repeated point
645 .append(s23).append(vert[i+2].y).append(L)
646 .append(s33).append(vert[i+2].z).append(L);
650 static private final int MAX_THREADS = Runtime.getRuntime().availableProcessors();
651 static private final Vector v_threads = new Vector(MAX_THREADS); // synchronized
653 /** Creates a mesh for the given Displayable in a separate Thread. */
654 private Thread addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
655 final double scale = this.scale;
656 Thread thread = new Thread() {
657 public void run() {
658 setPriority(Thread.NORM_PRIORITY);
659 while (v_threads.size() >= MAX_THREADS) { // this is crude. Could do much better now ... much better! Properly queued tasks.
660 try { Thread.sleep(400); } catch (InterruptedException ie) {}
662 v_threads.add(this);
663 try {
665 // 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.)
666 List triangles = null;
667 if (displ instanceof AreaList) {
668 int rs = resample;
669 if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
670 else rs = Display3D.this.resample;
671 triangles = ((AreaList)displ).generateTriangles(scale, rs);
672 //triangles = removeNonManifold(triangles);
673 } else if (displ instanceof Ball) {
674 double[][][] globe = Ball.generateGlobe(12, 12);
675 triangles = ((Ball)displ).generateTriangles(scale, globe);
676 } else if (displ instanceof Line3D) {
677 // Pipe and Polyline
678 // adjustResampling(); // fails horribly, needs first to correct mesh-generation code
679 triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
680 } else if (null == displ && pt.getType().equals("profile_list")) {
681 triangles = Profile.generateTriangles(pt, scale);
683 // safety checks
684 if (null == triangles) {
685 Utils.log("Some error ocurred: can't create triangles for " + displ);
686 return;
688 if (0 == triangles.size()) {
689 Utils.log2("Skipping empty mesh for " + displ.getTitle());
690 return;
692 if (0 != triangles.size() % 3) {
693 Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
694 return;
696 Color color = null;
697 float alpha = 1.0f;
698 if (null != displ) {
699 color = displ.getColor();
700 alpha = displ.getAlpha();
701 } else {
702 // for profile_list: get from the first (what a kludge)
703 Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
704 if (null == obp) return;
705 Displayable di = (Displayable)obp;
706 color = di.getColor();
707 alpha = di.getAlpha();
709 // add to 3D view (synchronized)
710 synchronized (u_lock) {
711 u_lock.lock();
712 try {
713 // craft a unique title (id is always unique)
714 String title = null == displ ? pt.toString() + " #" + pt.getId() : makeTitle(displ);
715 if (ht_pt_meshes.contains(pt)) {
716 // remove content from universe
717 universe.removeContent(title);
718 // no need to remove entry from table, it's overwritten below
720 // register mesh
721 ht_pt_meshes.put(pt, triangles);
722 // ensure proper default transform
723 //universe.resetView();
725 //universe.ensureScale((float)(width*scale));
726 universe.addMesh(triangles, new Color3f(color), title, 1); // had a (float)(width*scale) param in there before the 1
727 Content ct = universe.getContent(title);
728 ct.setTransparency(1f - alpha);
729 ct.toggleLock();
730 } catch (Exception e) {
731 IJError.print(e);
733 u_lock.unlock();
736 Utils.log2(pt.toString() + " n points: " + triangles.size());
738 } catch (Exception e) {
739 IJError.print(e);
740 } finally {
741 v_threads.remove(this);
744 } // end of run
746 thread.start();
747 return thread;
750 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
751 static public Thread addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
752 return addMesh(ref_ls, vs, title, color, null, 1.0f);
755 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
756 static public Thread addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha) {
757 Thread thread = new Thread() {
758 public void run() {
759 setPriority(Thread.NORM_PRIORITY);
760 while (v_threads.size() >= MAX_THREADS) { // this is crude. Could do much better now ... much better! Properly queued tasks.
761 try { Thread.sleep(50); } catch (InterruptedException ie) {}
763 v_threads.add(this);
764 try {
765 /////
766 Display3D d3d = null;
767 d3d = Display3D.get(ref_ls);
768 final double scale = d3d.scale;
769 final double width = d3d.width;
770 float transp = 1 - alpha;
771 if (transp < 0) transp = 0;
772 if (transp > 1) transp = 1;
773 if (1 == transp) {
774 Utils.log("WARNING: adding a 3D object fully transparent.");
777 double[] wi = widths;
778 if (null == widths) {
779 wi = new double[vs.getPoints(0).length];
780 //Utils.log2("len: " + wi.length + vs.getPoints(0).length + vs.getPoints(1).length);
781 Arrays.fill(wi, 2.0);
782 } else if (widths.length != vs.length()) {
783 Utils.log("ERROR: widths.length != VectorString3D.length()");
784 return;
787 List triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), scale);
789 // add to 3D view (synchronized)
790 synchronized (d3d.u_lock) {
791 d3d.u_lock.lock();
792 try {
793 // ensure proper default transform
794 //d3d.universe.resetView();
796 //Utils.log2(title + " : vertex count % 3 = " + triangles.size() % 3 + " for " + triangles.size() + " vertices");
797 //d3d.universe.ensureScale((float)(width*scale));
798 d3d.universe.addMesh(triangles, new Color3f(color), title, /*(float)(width*scale),*/ 1);
799 Content ct = d3d.universe.getContent(title);
800 ct.setTransparency(transp);
801 ct.toggleLock();
802 } catch (Exception e) {
803 IJError.print(e);
805 d3d.u_lock.unlock();
808 /////
809 } catch (Exception e) {
810 IJError.print(e);
811 } finally {
812 v_threads.remove(this);
815 } // end of run
817 thread.start();
818 return thread;
821 // This method has the exclusivity in adjusting the resampling value.
822 synchronized private final int adjustResampling() {
823 if (resample > 0) return resample;
824 final GenericDialog gd = new GenericDialog("Resample");
825 gd.addSlider("Resample: ", 1, 20, -1 != resample ? resample : DEFAULT_RESAMPLE);
826 gd.showDialog();
827 if (gd.wasCanceled()) {
828 resample = -1 != resample ? resample : DEFAULT_RESAMPLE; // current or default value
829 return resample;
831 resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
832 return resample;
835 /** Checks if there is any Display3D instance currently showing the given Displayable. */
836 static public boolean isDisplayed(final Displayable d) {
837 if (null == d) return false;
838 final String title = makeTitle(d);
839 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
840 Display3D d3d = (Display3D)it.next();
841 if (null != d3d.universe.getContent(title)) return true;
843 return false;
846 static public void setColor(final Displayable d, final Color color) {
847 if (!isDisplayed(d)) return;
848 Display3D d3d = get(d.getLayer().getParent());
849 if (null == d3d) return; // no 3D displays open
850 Content content = d3d.universe.getContent(makeTitle(d));
851 //Utils.log2("content: " + content);
852 if (null != content) content.setColor(new Color3f(color));
855 static public void setTransparency(final Displayable d, final float alpha) {
856 if (null == d) return;
857 Layer layer = d.getLayer();
858 if (null == layer) return; // some objects have no layer, such as the parent LayerSet.
859 Object ob = ht_layer_sets.get(layer.getParent());
860 if (null == ob) return;
861 Display3D d3d = (Display3D)ob;
862 String title = makeTitle(d);
863 Content content = d3d.universe.getContent(title);
864 if (null != content) content.setTransparency(1 - alpha);
865 else if (null == content && d.getClass().equals(Patch.class)) {
866 Patch pa = (Patch)d;
867 if (pa.isStack()) {
868 title = pa.getProject().getLoader().getFileName(pa);
869 for (Iterator it = Display3D.ht_layer_sets.values().iterator(); it.hasNext(); ) {
870 d3d = (Display3D)it.next();
871 for (Iterator cit = d3d.universe.getContents().iterator(); cit.hasNext(); ) {
872 Content c = (Content)cit.next();
873 if (c.getName().startsWith(title)) {
874 c.setTransparency(1 - alpha);
875 // no break, since there could be a volume and an orthoslice
883 static public String makeTitle(final Displayable d) {
884 return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
886 static public String makeTitle(final Patch p) {
887 return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
888 + " #" + p.getProject().getLoader().getNextId();
891 /** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
892 * (otherwise returns null). */
893 static public Thread update(final Displayable d) {
894 Layer layer = d.getLayer();
895 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
896 Object ob = ht_layer_sets.get(layer.getParent());
897 if (null == ob) return null;
898 Display3D d3d = (Display3D)ob;
899 return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
903 static public final double computeTriangleArea() {
904 return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
905 Math.pow(yA*zB + yB*zC + yC*zA, 2) +
906 Math.pow(zA*xB + zB*xC + zC*xA, 2));
910 static public final boolean contains(final LayerSet ls, final String title) {
911 final Display3D d3d = getDisplay(ls);
912 if (null == d3d) return false;
913 return null != d3d.universe.getContent(title);