/* $RCSfile$
 * $Author: hansonr $
 * $Date: 2007-05-18 15:41:42 -0500 (Fri, 18 May 2007) $
 * $Revision: 7752 $

 *
 * Copyright (C) 2003-2005  The Jmol Development Team
 *
 * Contact: jmol-developers@lists.sf.net
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.jmol.export;


import java.util.Hashtable;
import java.util.Map;

import javajs.util.A4;
import javajs.util.AU;
import javajs.util.Lst;
import javajs.util.Measure;
import javajs.util.P3;
import javajs.util.PT;
import javajs.util.Quat;
import javajs.util.T3;
import javajs.util.V3;

import javajs.util.BS;

import org.jmol.util.Font;
import org.jmol.util.GData;
import org.jmol.util.Geodesic;
import org.jmol.viewer.Viewer;

/**
 * A basic VRML generator. Modified 10/2016 to no longer
 * use high-level objects Cone, Sphere, and Cylinder. 
 * 
 * Makes substantial use of DEF and USE to reduce file size
 * hugely.
 *  
 *  
 * @author Bob Hanson 
 */
public class _VrmlExporter extends __CartesianExporter {

  /*
   * 1/2009 Angel Herraez: # added support for translucency # Jmol info in
   * header # set navigation mode # added support for background color # added
   * support for labels: text, font face and font style; size is hardcoded to
   * 0.4A
   */

  public _VrmlExporter() {
    useTable = new UseTable("USE ");
    commentChar = "# ";
    canCapCylinders = true;
    solidOnly = true;
  }
  
  @Override
  protected void output(T3 pt) {
    output(round(pt));
  }
  
  protected UseTable useTable;
  
  @Override
  protected void outputHeader() {
    output("#VRML V2.0 utf8 Generated by Jmol " + Viewer.getJmolVersion()
        + "\n");
    output("WorldInfo { \n");
    output(" title " + PT.esc(vwr.ms.modelSetName) + "\n");
    output(" info [ \"Generated by Jmol " + Viewer.getJmolVersion() + " \", \n");
    output("  \"http://www.jmol.org \", \n");
    output("  \"Creation date: " + getExportDate() + " \" ]\n");
    output("} \n");

    output("NavigationInfo { type \"EXAMINE\" } \n");
    // puts the vwr into model-rotation mode
    output("Background { skyColor [" + rgbFractionalFromColix(backgroundColix)
        + "] } \n");
    // next is an approximation only
    float angle = getViewpoint();
    output("Viewpoint{fieldOfView " + angle);
    output(" position ");
    // remove export scaling for from Viewpoint so on-screen version is good.
    cameraPosition.scale(exportScale);
    output(cameraPosition);
    output(" orientation ");
    output(tempP1);
    output(" " + -viewpoint.angle);
    output("\n jump TRUE description \"v1\"\n}\n\n");
    output(getJmolPerspective());
    outputInitialTransform();
    output("\n");
  }

  protected void outputInitialTransform() {
    pushMatrix();
    outputAttr("scale", exportScale, exportScale, exportScale);
    outputCloseTag();
    outputChildStart();
    pushMatrix();
    tempP1.setT(center);
    tempP1.scale(-1);
    
    outputAttrPt("translation", tempP1);
    outputCloseTag();
    outputChildStart();
  }
  
  protected float getViewpoint() {
    viewpoint.setM(vwr.tm.matrixRotate);
    tempP1.set(viewpoint.x, viewpoint.y, (viewpoint.angle == 0 ? 1
        : viewpoint.z));
    return (float) (apertureAngle * Math.PI / 180);
  }  
 
  @Override
  protected void outputFooter() {
    useTable = null;
    output("\n]}\n");
    output("]}"); // scale
  }

  protected void outputAppearance(short colix, boolean isText) {
    String def = getDef((isText ? "T" : "") + colix);
    output("appearance ");
    if (def.charAt(0) == '_') {
      String color = rgbFractionalFromColix(colix);
      output(" DEF " + def + " Appearance{material Material{diffuseColor ");
      if (isText)
        output(" 0 0 0 specularColor 0 0 0 ambientIntensity 0.0 shininess 0.0 emissiveColor " 
            + color + " }}");
      else
        output(color + " transparency " + translucencyFractionalFromColix(colix) + "}}");
      return;
    }
    output(def);
  }
  
  protected void pushMatrix() {
    output("Transform{");
  }

  protected void popMatrix() {
    output("}\n");
  }

  protected void outputAttrPt(String attr, T3 pt) {
    output(" " + attr + " " + pt.x + " " + pt.y + " " + pt.z);
  }

  protected void outputAttr(String attr, float x, float y, float z) {
    output(" " + attr + " " + round(x) + " " + round(y) + " " + round(z));
  }
  
  protected void outputRotation(A4 a) {
    output(" rotation " + a.x + " " + a.y + " " + a.z + " " + a.angle);
  }

  protected void outputTransRot(P3 pt1, P3 pt2, int x, int y, int z) {
    tempV1.ave(pt2, pt1);
    outputAttrPt("translation", tempV1);
    tempV1.sub(pt1);
    tempV1.normalize();
    tempV2.set(x, y, z);
    tempV2.add(tempV1);
    outputRotation(A4.newVA(tempV2, (float) Math.PI));
  }

  protected void outputQuaternionFrame(P3 ptCenter, P3 ptX, P3 ptY, P3 ptZ,
                                       float xScale, float yScale,
                                       float zScale) {

    //Hey, hey -- quaternions to the rescue!
    // Just send three points to Quaternion to define a plane and return
    // the AxisAngle required to rotate to that position. That's all there is to it.

    tempQ1.setT(ptX);
    tempQ2.setT(ptY);
    A4 a = Quat.getQuaternionFrame(ptCenter, tempQ1, tempQ2).toAxisAngle4f();
    if (!Float.isNaN(a.x)) {
      tempQ1.set(a.x, a.y, a.z);
      outputRotation(a);
    }
    float sx =  (ptX.distance(ptCenter) * xScale);
    float sy =  (ptY.distance(ptCenter) * yScale);
    float sz =  (ptZ.distance(ptCenter) * zScale);
    outputAttr("scale", sx, sy, sz);
  }

  protected void outputChildShapeStart() {
    outputChildStart();
    outputShapeStart();
  }

  protected void outputChildStart() {
    output(" children[");
  }

  protected void outputShapeStart() {
    output(" Shape{geometry ");
  }

  protected void outputDefChildFaceSet(String child) {
    if (child != null)
      output("DEF " + child + " ");
    outputFaceSetStart();
  }

  protected void outputFaceSetStart() {
    output("IndexedFaceSet {");
  }

  protected void outputFaceSetClose() {
    output("}\n");
  }

  protected void outputUseChildClose(String child) {
    output(child);
  }
  
  protected void outputChildShapeClose() {
    outputShapeClose();
    outputChildClose();
  }

  protected void outputChildClose() {
    output("]");
  }

  protected void outputShapeClose() {
    output("}");
  }

  protected void outputCloseTag() {
    // nothing to do
  }

  @Override
  protected void outputCircle(P3 pt1, P3 pt2, float radius, short colix,
                            boolean doFill) {
    // not in _StlExport
    // not fixed -- still duplicated in X3d
    if (doFill) {
      // draw filled circle
      pushMatrix();
        tempV1.ave(pt1, pt2);
        outputAttr("translation", tempV1.x, tempV1.y, tempV1.z);
        output(" children [ Billboard{axisOfRotation 0 0 0 children [ Transform{rotation 1 0 0 1.5708");
          float height = (pt1.distance(pt2));
          outputAttr("scale", radius, height, radius);
          outputCylinderChildScaled(colix, GData.ENDCAPS_FLAT);
        output("}] }]\n");
      popMatrix();
      return;
    }

    // draw a thin torus

    String child = getDef("C" + colix + "_" + radius);
    pushMatrix();
      outputTransRot(pt1, pt2, 0, 0, 1);
      outputAttr("scale", radius, radius, radius);
      output(" children [");
        if (child.charAt(0) == '_') {
          output("DEF " + child);
          output(" Billboard{axisOfRotation 0 0 0 children [ Transform{children[");
            output(" Shape{");
              output("geometry Extrusion{beginCap FALSE convex FALSE endCap FALSE creaseAngle 1.57");
                output(" crossSection [");
                float rpd = 3.1415926f / 180;
                float scale = 0.02f / radius;
                for (int i = 0; i <= 360; i += 10) {
                  output(round(Math.cos(i * rpd) * scale) + " ");
                  output(round(Math.sin(i * rpd) * scale) + " ");
                }
                output("] spine [");
                for (int i = 0; i <= 360; i += 10) {
                  output(round(Math.cos(i * rpd)) + " ");
                  output(round(Math.sin(i * rpd)) + " 0 ");
                }
                output("]");
              output("}");
              outputAppearance(colix, false);
            output("}");
          output("]} ]}");
        } else {
          output(child);
        }
      output("]");
    popMatrix();
  }

  @Override
  protected void outputCone(P3 ptBase, P3 ptTip, float radius,
                            short colix) {
    float height = (ptBase.distance(ptTip));
    pushMatrix();
      outputTransRot(ptBase, ptTip, 0, 1, 0);
      outputAttr("scale", radius, height, radius);
      outputCloseTag();
      outputChildShapeStart();
        String child = getDef("c");
        if (child.charAt(0) == '_') {
          outputDefChildFaceSet(child);
          outputConeGeometry(true);
          outputFaceSetClose();
        } else {
          outputUseChildClose(child);  
        }
        outputAppearance(colix, false);
      outputChildShapeClose();
    popMatrix();
  }

  private void outputConeGeometry(boolean addBase) {
    //output("DEF " + cone + " Cone{height " + round(height)
      //     + " bottomRadius " + round(radius) + "}");
    int ndeg = 10;
    int n = 360 / ndeg;
    int vertexCount = n + (addBase ? 2 : 1);
    int[][] faces = AU.newInt2(n * (addBase ? 2 : 1));
    for (int i = 0, fpt = 0; i < n; i++) {
      faces[fpt++] = new int[] { i, (i + n - 1) % n, n };
      if (addBase)
        faces[fpt++] = new int[] { i, (i + 1) % n, n + 1 };   
    }
    P3[] vertexes = new P3[vertexCount];
    for (int i = 0; i < n; i++) {
      float x = (float) (Math.cos(i * ndeg / 180. * Math.PI)); 
      float y = (float) (Math.sin(i * ndeg / 180. * Math.PI)); 
      vertexes[i] = P3.new3(x, -0.5f, y);
    }    
    vertexes[n++] = P3.new3(0, 0.5f, 0);
    if (addBase)
      vertexes[n++] = P3.new3(0, -0.5f, 0);
    outputGeometry(vertexes, null, null, faces, null, vertexCount, faces.length, null, 3,
        null, null, null);
  }

  @Override
  protected boolean outputCylinder(P3 ptCenter, P3 pt1, P3 pt2, short colix,
                                   byte endcaps, float radius, P3 ptX, P3 ptY,
                                   boolean checkRadius) {
    float height = (pt1.distance(pt2));
    if (radius < 0.01f || height == 0)
      return false; // nucleic edges are 0.005f
    pushMatrix();
    if (ptX == null) {
      outputTransRot(pt1, pt2, 0, 1, 0);
      outputAttr("scale", radius, height, radius);
    } else {
      // arcs of ellipsoid rendering only
      outputAttrPt("translation", ptCenter);
      outputQuaternionFrame(ptCenter, ptY, pt1, ptX, 2, 2, 2);
      pt1.set(0, 0, -0.5f);
      pt2.set(0, 0, 0.5f);
    }
    outputCloseTag();
    outputCylinderChildScaled(colix, endcaps);
    popMatrix();
    if (radius > 0.1f)
      switch (endcaps) {
      case GData.ENDCAPS_SPHERICAL:
        outputSphere(pt1, radius * 1.01f, colix, checkRadius);
        //$FALL-THROUGH$
      case GData.ENDCAPS_FLAT_TO_SPHERICAL:
      case GData.ENDCAPS_OPEN_TO_SPHERICAL:
        outputSphere(pt2, radius * 1.01f, colix, checkRadius);
        break;
      case GData.ENDCAPS_FLAT:
        break;
      }
    return true;
  }
  
  protected void outputCylinderChildScaled(short colix, byte endcaps) {
    outputChildShapeStart();
    String child = getDef("C" + "_" + endcaps);
    if (child.charAt(0) == '_') {
      outputDefChildFaceSet(child);
      outputCylinderGeometry(endcaps);
      outputFaceSetClose();
    } else {
      outputUseChildClose(child);
    }
    outputAppearance(colix, false);
    outputChildShapeClose();
  }
  
  private void outputCylinderGeometry(int endcaps) {
    //" Cylinder{height " 
    //+ round(length) + " radius " + radius 
    //+ (endcaps == GData.ENDCAPS_FLAT ? "" : " top FALSE bottom FALSE") + "}");
    int ndeg = 10;
    int n = 360 / ndeg;
    int vertexCount = n * 2;
    boolean addEndcaps = false;
    switch (endcaps) {  
    case GData.ENDCAPS_SPHERICAL:
    case GData.ENDCAPS_FLAT_TO_SPHERICAL:
    case GData.ENDCAPS_OPEN_TO_SPHERICAL:
      // TODO  endcaps in VRML, X3D, STL
    case GData.ENDCAPS_FLAT:
      vertexCount += 2;
      addEndcaps = true;
      break;
    }
    int[][] faces = AU.newInt2(n * (addEndcaps ? 4 : 2));
    for (int i = 0, fpt = 0; i < n; i++) {
      faces[fpt++] = new int[] { i, (i + 1) % n, i + n };
      faces[fpt++] = new int[] { (i + 1) % n, (i + 1) % n + n, i + n };
      if (addEndcaps) {
        faces[fpt++] = new int[] { i, (i + n - 1) % n, vertexCount - 2 };
        faces[fpt++] = new int[] { i + n, (i + n + 1) % n + n, vertexCount - 1 };
      }
    }
    P3[] vertexes = new P3[vertexCount];
    for (int i = 0; i < n; i++) {
      float x = (float) (Math.cos(i * ndeg / 180. * Math.PI)); 
      float y = (float) (Math.sin(i * ndeg / 180. * Math.PI)); 
      vertexes[i] = P3.new3(x, 0.5f, y);
    }
    for (int i = 0; i < n; i++) {
      float x = (float) (Math.cos((i + 0.5) * ndeg / 180 * Math.PI)); 
      float y = (float) (Math.sin((i + 0.5) * ndeg / 180 * Math.PI)); 
      vertexes[i + n] = P3.new3(x, -0.5f, y);
    }
    if (addEndcaps) {
      vertexes[vertexCount - 2] = P3.new3(0, 0.5f, 0);
      vertexes[vertexCount - 1] = P3.new3(0, -0.5f, 0);
    }
    outputGeometry(vertexes, null, null, faces, null, vertexCount, faces.length, null, 3,
        null, null, null);
  }

  private Map<String, Boolean> htSpheresRendered = new Hashtable<String, Boolean>();

  @Override
  protected void outputSphere(P3 ptCenter, float radius, short colix, boolean checkRadius) {
    String check = round(ptCenter) + (checkRadius ? " " + (int) (radius * 100) : "");
    if (htSpheresRendered.get(check) != null)
      return;
    htSpheresRendered.put(check, Boolean.TRUE);
    outputSphereChildScaled(ptCenter, radius, null, colix);
  }

  @Override
  protected void outputEllipsoid(P3 ptCenter, P3[] points, short colix) {
    outputSphereChildScaled(ptCenter, 1.0f, points, colix);
  }

  private void outputSphereChildScaled(P3 ptCenter, float radius,
                                         P3[] points, short colix) {

    // Hey, hey -- quaternions to the rescue!
    // Just send three points to Quaternion to define a plane and return
    // the AxisAngle required to rotate to that position. That's all there is to
    // it.

    pushMatrix();
    outputAttrPt("translation", ptCenter);
    if (points == null)
      outputAttr("scale", radius, radius, radius);
    else
      outputQuaternionFrame(ptCenter, points[1], points[3], points[5], 1, 1, 1);
    outputCloseTag();
    outputChildShapeStart();
    String child = getDef("S");
    if (child.charAt(0) == '_') {
      outputDefChildFaceSet(child);
      outputSphereGeometry();
      outputFaceSetClose();
    } else {
      outputUseChildClose(child);
    }
    outputAppearance(colix, false);
    outputChildShapeClose();
    popMatrix();
  }
  
  private void outputSphereGeometry() {
    //was output(" Shape{geometry Sphere{radius " + radius + "}");
    
    V3[] vertices = Geodesic.getVertexVectors();
    int nVertices = 162;
    short[] faceList = Geodesic.getFaceVertexes(2);
    int nFaces = faceList.length / 3;
    int[][] indices = new int[nFaces][3];
    for (int i = 0, p = 0; i < nFaces; i++)
      for (int j = 0; j < 3; j++)
        indices[i][j] = faceList[p++];
    outputGeometry(vertices, null, null, indices, null, nVertices, 
        nFaces, null, 3, null, null, null);    
  }

  private T3[] plateVertices;
  private int[][] plateIndices;
  private short[] plateColixes;

  @Override
  protected void outputSolidPlate(P3 tempP1, P3 tempP2, P3 tempP3, short colix) {
    // nucleic plates
    //  0   1    2
    // 
    //  3   4    5
    if (plateVertices == null) {
      plateVertices = new P3[6];
      for (int i = 0; i < 6; i++)
        plateVertices[i] = new P3();
      //plateColixes = new short[8];
      plateIndices = new int[][] { { 0, 1, 2 }, { 5, 4, 3 }, { 0, 3, 1 },
          { 1, 3, 4 }, { 1, 4, 2 }, { 2, 4, 5 }, { 2, 5, 0 }, { 0, 5, 3 } };
    }
    Measure.calcNormalizedNormal(tempP1, tempP2, tempP3, tempV1, tempV2);
    tempV1.scale(0.2f);
    plateVertices[0].setT(tempP1);
    plateVertices[1].setT(tempP2);
    plateVertices[2].setT(tempP3);
    for (int i = 0; i < 3; i++)
      plateVertices[i].add(tempV1);
    tempV1.scale(-2);
    for (int i = 3; i < 6; i++)
      plateVertices[i].add2(plateVertices[i - 3], tempV1);
    //for (int i = 0; i < 8; i++)
    //plateColixes[i] = colix;

    outputSurface(plateVertices, null, null, plateIndices, plateColixes, 6, 8,
        8, null, 3, colix, null, null, null);
  }


  protected P3 tempQ1 = new P3();
  protected P3 tempQ2 = new P3();
  protected P3 tempQ3 = new P3();

  @Override
  protected void outputSurface(T3[] vertices, T3[] normals, short[] colixes,
                               int[][] indices, short[] polygonColixes,
                               int nVertices, int nPolygons, int nTriangles,
                               BS bsPolygons, int faceVertexMax, short colix,
                               Lst<Short> colorList,
                               Map<Short, Integer> htColixes, P3 offset) {
    outputShapeStart();
    outputDefChildFaceSet(null);
    outputGeometry(vertices, normals, colixes, indices, polygonColixes,
        nVertices, nPolygons, bsPolygons, faceVertexMax, colorList, htColixes,
        offset);
    outputFaceSetClose();
    outputAppearance(colix, false);
    outputShapeClose();
  }

  protected void outputGeometry(T3[] vertices, T3[] normals,
                                short[] colixes, int[][] indices,
                                short[] polygonColixes,
                                int nVertices, int nPolygons, BS bsPolygons,
                                int faceVertexMax,
                                Lst<Short> colorList, Map<Short, Integer> htColixes, P3 offset) {
     if (polygonColixes == null) 
       output("  creaseAngle 0.5  \n");
     else
       output(" colorPerVertex FALSE\n");

     // coordinates

     output("coord Coordinate {\npoint [\n");
     outputVertices(vertices, nVertices, offset);
     output("   ]\n");
     output("  }\n");
     output("  coordIndex [\n");
     int[] map = new int[nVertices];
     getCoordinateMap(vertices, map, null);
     outputIndices(indices, map, nPolygons, bsPolygons, faceVertexMax);
     output("  ]\n");

     // normals

     if (normals != null) {
       Lst<String> vNormals = new  Lst<String>();
       map = getNormalMap(normals, nVertices, null, vNormals);
       output("  solid FALSE\n  normalPerVertex TRUE\n   normal Normal {\n  vector [\n");
       outputNormals(vNormals);
       output("   ]\n");
       output("  }\n");
       output("  normalIndex [\n");
       outputIndices(indices, map, nPolygons, bsPolygons, faceVertexMax);
       output("  ]\n");
     }

     map = null;
     
     // colors

     if (colorList != null) {
       output("  color Color { color [\n");
       outputColors(colorList);
       output("  ] } \n");
       output("  colorIndex [\n");
       outputColorIndices(indices, nPolygons, bsPolygons, faceVertexMax, htColixes, colixes, polygonColixes);
       output("  ]\n");
     }
   }

   @Override
   protected void outputFace(int[] face, int[] map, int faceVertexMax) {
     output(map[face[0]] + " " + map[face[1]] + " " + map[face[2]] + " -1\n");
     if (faceVertexMax == 4 && face.length == 4)
       output(map[face[0]] + " " + map[face[2]] + " " + map[face[3]] + " -1\n");
   }

   protected void outputNormals(Lst<String> vNormals) {
     int n = vNormals.size();
     for (int i = 0; i < n; i++) {
       output(vNormals.get(i));
     }
   }

   protected void outputColors(Lst<Short> colorList) {
     int nColors = colorList.size();
     for (int i = 0; i < nColors; i++) {
       String color = rgbFractionalFromColix(colorList.get(i).shortValue());
       output(" ");
       output(color);
       output("\n");
     }
   }

   protected void outputColorIndices(int[][] indices, int nPolygons, BS bsPolygons,
                                   int faceVertexMax, Map<Short, Integer> htColixes,
                                   short[] colixes, short[] polygonColixes) {
     boolean isAll = (bsPolygons == null);
     int i0 = (isAll ? nPolygons - 1 : bsPolygons.nextSetBit(0));
     for (int i = i0; i >= 0; i = (isAll ? i - 1 : bsPolygons.nextSetBit(i + 1))) {
       if (polygonColixes == null) {
         output(htColixes.get(Short.valueOf(colixes[indices[i][0]])) + " "
             + htColixes.get(Short.valueOf(colixes[indices[i][1]])) + " "
             + htColixes.get(Short.valueOf(colixes[indices[i][2]])) + " -1\n");
         if (faceVertexMax == 4 && indices[i].length == 4)
           output(htColixes.get(Short.valueOf(colixes[indices[i][0]])) + " "
               + htColixes.get(Short.valueOf(colixes[indices[i][2]])) + " "
               + htColixes.get(Short.valueOf(colixes[indices[i][3]])) + " -1\n");
       } else {
         output(htColixes.get(Short.valueOf(polygonColixes[i])) + "\n");
       }
     }
   }

  private int[][] oneFace;
  private P3[] threeVertices;
  @Override
  protected void outputTriangle(T3 pt1, T3 pt2, T3 pt3, short colix) {  
    
    // TODO  -- this is a problem - not a closed surface
    
    // from __CartesianExporter.fillTriangle only

    output("Shape{geometry IndexedFaceSet{ "); 
    outputTriangleGeometry(pt1, pt2, pt3, colix);    
    output("}\n");
    outputAppearance(colix, false);
    output("}\n");
  }

  private void outputTriangleGeometry(T3 pt1, T3 pt2, T3 pt3, @SuppressWarnings("unused") short colix) {
    if (oneFace == null) {
      oneFace = new int[][] {new int[] { 0, 1, 2 }};
      threeVertices = new P3[] { tempP1, tempP2, tempP3 };
    }
    threeVertices[0].setT(pt1);
    threeVertices[1].setT(pt2);
    threeVertices[2].setT(pt3);
    outputGeometry(threeVertices, null, null, oneFace, null, 3, 1, null, 3, null, null, null);
  }

  @Override
  protected void outputTextPixel(P3 pt, int argb) {
    // labels and images only -- ignore here
//    String color = rgbFractionalFromArgb(argb);
//    output("Transform{translation ");
//    output(pt);
//    output(" children ");
//    String child = getDef("p" + argb);
//    if (child.charAt(0) == '_') {
//      output("DEF " + child + " Shape{geometry Sphere{radius 0.01}");
//      output(" appearance Appearance{material Material{diffuseColor 0 0 0 specularColor 0 0 0 ambientIntensity 0.0 shininess 0.0 emissiveColor "
//          + color + " }}}");
//    } else {
//      output(child);
//    }
//    output("}\n");
  }

  protected float fontSize;
  protected String fontFace;
  protected String fontStyle;
  protected String fontChild;

  @Override
  void plotText(int x, int y, int z, short colix, String text, Font font3d) {
    // not in _StlExport
    // not worked out for X3D
    pushMatrix();
    tempP3.set(x, y, fixScreenZ(z));
    tm.unTransformPoint(tempP3, tempP1);
    outputAttrPt("translation", tempP1);
    setFont(colix, text, font3d);
    // These x y z are 3D coordinates of echo or the atom that the label is attached to
    outputChildStart();
    
    if (fontChild.charAt(0) == '_') {
      output("DEF " + fontChild + " Billboard{");
      outputAttr("axisOfRotation", 0, 0, 0);
      outputChildStart();
      pushMatrix();
      outputChildShapeStart();
      output("Text{fontStyle ");
      String fontstyle = getDef("F" + fontFace + fontStyle);
      if (fontstyle.charAt(0) == '_') {
        output("DEF " + fontstyle + " FontStyle{size " + fontSize
            + " family \"" + fontFace + "\" style \"" + fontStyle + "\"}");
      } else {
        output(fontstyle);
      }
      output(" string " + PT.esc(text) + "}"); 
      outputAppearance(colix, true);
      outputChildShapeClose();
      popMatrix();
      outputChildClose();
      output("}");
    } else {
      output(fontChild);
    }
    outputChildClose();
    popMatrix();
  }

  private void setFont(short colix, String text, Font font3d) {
    fontStyle = font3d.fontStyle.toUpperCase();
    fontFace = font3d.fontFace.toUpperCase();
    fontFace = (fontFace.equals("MONOSPACED") ? "TYPEWRITER" 
        : fontFace.equals("SERIF") ? "SERIF" : "Arial");
    fontSize = font3d.fontSize * 0.015f;
    fontChild = getDef("T" + colix + fontFace + fontStyle + fontSize + "_" + text);
  }

  protected String getDef(String key) {
    return (useTable == null ? "_" : useTable.getDef(key));
  }

  /*
   * Unsolved issues: # Non-label texts: echos, measurements :: need to get
   * space coordinates, not screen coord. # Font size: not implemented; 0.4A
   * is hardcoded (resizes with zoom) Java VRML font3d.fontSize = 13.0 size
   * (numeric), but in angstroms, not pixels font3d.fontSizeNominal = 13.0 #
   * Label offsets: not implemented; hardcoded to 0.25A in each x,y,z #
   * Multi-line labels: only the first line is received # Sub/superscripts not
   * interpreted
   */

}


