private Point3d computeWinCoord(
      Canvas3D canvas,
      RenderAtom ra,
      Point2d winCoord,
      Point3d objCoord,
      Transform3D localToImagePlate) {
    // Get local to Vworld transform
    RenderMolecule rm = ra.renderMolecule;

    if (rm == null) {
      // removeRenderAtom() may set ra.renderMolecule to null
      // in RenderBin before this renderer thread run.
      return null;
    }

    // MT safe issue: We can't reference ra.renderMolecule below since
    // RenderBin thread may set it to null anytime. Use rm instead.

    Transform3D lvw = rm.localToVworld[rm.localToVworldIndex[NodeRetained.LAST_LOCAL_TO_VWORLD]];

    Point3d clipCoord3 = new Point3d();
    clipCoord3.set(objCoord);
    Point4d clipCoord4 = new Point4d();

    // Transform point from local coord. to clipping coord.
    lvw.transform(clipCoord3);
    canvas.vworldToEc.transform(clipCoord3);
    canvas.projTrans.transform(clipCoord3, clipCoord4);

    // clip check in Z
    if ((clipCoord4.w <= 0.0) || (clipCoord4.z > clipCoord4.w) || (-clipCoord4.z > clipCoord4.w)) {

      return null;
    }
    double invW = 1.0 / clipCoord4.w;

    clipCoord3.x = clipCoord4.x * invW;
    clipCoord3.y = clipCoord4.y * invW;
    clipCoord3.z = clipCoord4.z * invW;

    // Get Vworld to image plate Xform
    canvas.getLastVworldToImagePlate(localToImagePlate);

    // v' = vwip x lvw x v
    // 		where v' = transformed vertex,
    // 			  lvw = local to Vworld Xform
    //			  vwip = Vworld to Image plate Xform
    //			  v = vertex

    // Compute composite local to image plate Xform
    localToImagePlate.mul(lvw);

    // Transform the Raster's position from object to world coordinates
    localToImagePlate.transform(objCoord);

    // Get the window coordinates of this point
    canvas.getPixelLocationFromImagePlate(objCoord, winCoord);

    return clipCoord3;
  }
  // TODO -- Need to rethink. Might have to consider charTransform[] in returns pickInfo.
  @Override
  boolean intersect(
      PickShape pickShape,
      PickInfo pickInfo,
      int flags,
      Point3d iPnt,
      GeometryRetained geom,
      int geomIndex) {
    Transform3D tempT3D = new Transform3D();
    GeometryArrayRetained geo = null;
    int sIndex = -1;
    PickShape newPS;
    double minDist = Double.MAX_VALUE;
    double distance = 0.0;
    Point3d closestIPnt = new Point3d();

    for (int i = 0; i < numChars; i++) {
      geo = geometryList[i];
      if (geo != null) {
        tempT3D.invert(charTransforms[i]);
        newPS = pickShape.transform(tempT3D);
        if (geo.intersect(newPS, pickInfo, flags, iPnt, geom, geomIndex)) {
          if (flags == 0) {
            return true;
          }
          distance = newPS.distance(iPnt);
          if (distance < minDist) {
            sIndex = i;
            minDist = distance;
            closestIPnt.set(iPnt);
          }
        }
      }
    }

    if (sIndex >= 0) {
      // We need to transform iPnt to the vworld to compute the actual distance.
      // In this method we'll transform iPnt by its char. offset. Shape3D will
      // do the localToVworld transform.
      iPnt.set(closestIPnt);
      charTransforms[sIndex].transform(iPnt);
      return true;
    }
    return false;
  }
 @Override
 void computeBoundingBox() {
   if (clipMode == Raster.CLIP_IMAGE) {
     // Disable view frustum culling by setting the raster's bounds to
     // infinity.
     Point3d minBounds =
         new Point3d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
     Point3d maxBounds =
         new Point3d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
     geoBounds.setUpper(maxBounds);
     geoBounds.setLower(minBounds);
   } else {
     Point3d center = new Point3d();
     center.x = position.x;
     center.y = position.y;
     center.z = position.z;
     geoBounds.setUpper(center);
     geoBounds.setLower(center);
   }
 }
  public static void main(final String[] args) {

    final int N_BLOBS = 20;
    final double RADIUS = 5; // µm
    final Random RAN = new Random();
    final double WIDTH = 100; // µm
    final double HEIGHT = 100; // µm
    final double DEPTH = 50; // µm
    final double[] CALIBRATION = new double[] {0.5f, 0.5f, 1};
    final AxisType[] AXES = new AxisType[] {Axes.X, Axes.Y, Axes.Z};

    // Create 3D image
    final Img<UnsignedByteType> source =
        new ArrayImgFactory<UnsignedByteType>()
            .create(
                new int[] {
                  (int) (WIDTH / CALIBRATION[0]),
                  (int) (HEIGHT / CALIBRATION[1]),
                  (int) (DEPTH / CALIBRATION[2])
                },
                new UnsignedByteType());
    final ImgPlus<UnsignedByteType> img =
        new ImgPlus<UnsignedByteType>(source, "Test", AXES, CALIBRATION);

    // Random blobs
    final double[] radiuses = new double[N_BLOBS];
    final ArrayList<double[]> centers = new ArrayList<double[]>(N_BLOBS);
    final int[] intensities = new int[N_BLOBS];
    double x, y, z;
    for (int i = 0; i < N_BLOBS; i++) {
      radiuses[i] = RADIUS + RAN.nextGaussian();
      x = WIDTH * RAN.nextFloat();
      y = HEIGHT * RAN.nextFloat();
      z = DEPTH * RAN.nextFloat();
      centers.add(i, new double[] {x, y, z});
      intensities[i] = RAN.nextInt(100) + 100;
    }

    // Put the blobs in the image
    for (int i = 0; i < N_BLOBS; i++) {
      final Spot tmpSpot =
          new Spot(centers.get(i)[0], centers.get(i)[1], centers.get(i)[2], radiuses[i], -1d);
      tmpSpot.putFeature(Spot.RADIUS, radiuses[i]);
      final SpotNeighborhood<UnsignedByteType> sphere =
          new SpotNeighborhood<UnsignedByteType>(tmpSpot, img);
      for (final UnsignedByteType pixel : sphere) {
        pixel.set(intensities[i]);
      }
    }

    // Instantiate detector
    final LogDetector<UnsignedByteType> detector =
        new LogDetector<UnsignedByteType>(
            img, img, TMUtils.getSpatialCalibration(img), RADIUS, 0, true, false);

    // Segment
    final long start = System.currentTimeMillis();
    if (!detector.checkInput() || !detector.process()) {
      System.out.println(detector.getErrorMessage());
      return;
    }
    final Collection<Spot> spots = detector.getResult();
    final long end = System.currentTimeMillis();

    // Display image
    ImageJFunctions.show(img);

    // Display results
    final int spot_found = spots.size();
    System.out.println("Segmentation took " + (end - start) + " ms.");
    System.out.println("Found " + spot_found + " blobs.\n");

    Point3d p1, p2;
    double dist, min_dist;
    int best_index = 0;
    double[] best_match;
    final ArrayList<Spot> spot_list = new ArrayList<Spot>(spots);
    Spot best_spot = null;
    final double[] coords = new double[3];
    final String[] posFeats = Spot.POSITION_FEATURES;

    while (!spot_list.isEmpty() && !centers.isEmpty()) {

      min_dist = Float.POSITIVE_INFINITY;
      for (final Spot s : spot_list) {

        int index = 0;
        for (final String pf : posFeats) {
          coords[index++] = s.getFeature(pf).doubleValue();
        }
        p1 = new Point3d(coords);

        for (int j = 0; j < centers.size(); j++) {
          p2 = new Point3d(centers.get(j));
          dist = p1.distance(p2);
          if (dist < min_dist) {
            min_dist = dist;
            best_index = j;
            best_spot = s;
          }
        }
      }

      spot_list.remove(best_spot);
      best_match = centers.remove(best_index);
      int index = 0;
      for (final String pf : posFeats) {
        coords[index++] = best_spot.getFeature(pf).doubleValue();
      }
      System.out.println("Blob coordinates: " + Util.printCoordinates(coords));
      System.out.println(
          String.format(
              "  Best matching center at distance %.1f with coords: "
                  + Util.printCoordinates(best_match),
              min_dist));
    }
    System.out.println();
    System.out.println("Unmatched centers:");
    for (int i = 0; i < centers.size(); i++)
      System.out.println("Center " + i + " at position: " + Util.printCoordinates(centers.get(i)));
  }
  @Override
  void execute(
      Canvas3D cv,
      RenderAtom ra,
      boolean isNonUniformScale,
      boolean updateAlpha,
      float alpha,
      int screen,
      boolean ignoreVertexColors) {

    // Compute the offset position of the raster
    // This has to be done at render time because we need access
    // to the Canvas3D info

    // Check if adjusted position needs to be computed
    Point3d adjPos = new Point3d(); // Position of the Raster after adjusting for dstOffset
    adjPos.set(position);

    Point2d winCoord = new Point2d(); // Position of Raster in window coordinates
    Transform3D localToImagePlate = new Transform3D(); // Local to Image plate transform

    Point3d clipCoord = computeWinCoord(cv, ra, winCoord, adjPos, localToImagePlate);

    // Test raster for out of bounds in Z.
    if (clipCoord == null) {
      return;
    }

    if (clipMode == Raster.CLIP_POSITION) {
      // Do trivial reject test on Raster position.
      if (!isRasterClipPositionInside(clipCoord)) {
        return;
      }
    }

    // Add the destination offset to the Raster position in window coordinates
    winCoord.x += xDstOffset;
    winCoord.y += yDstOffset;

    // System.err.println("Step 2 : adjPos " + adjPos + " winCoord " + winCoord);

    if ((type == Raster.RASTER_COLOR) || (type == Raster.RASTER_COLOR_DEPTH)) {
      float devCoordZ = (float) (clipCoord.z * 0.5 - 0.5);
      // Do textfill stuffs
      if (texture != null) {
        // setup Texture pipe.
        cv.updateTextureForRaster(texture);

        cv.textureFill(this, winCoord, devCoordZ, alpha);

        // Restore texture pipe.
        cv.restoreTextureBin();
      }
    }

    if ((type == Raster.RASTER_DEPTH) || (type == Raster.RASTER_COLOR_DEPTH)) {

      Point2i srcOffset = new Point2i(xSrcOffset, ySrcOffset);

      if (clipMode == Raster.CLIP_IMAGE) {
        clipImage(cv, ra, winCoord, srcOffset);
      }

      computeObjCoord(cv, winCoord, adjPos, localToImagePlate);

      cv.executeRasterDepth(
          cv.ctx,
          (float) adjPos.x,
          (float) adjPos.y,
          (float) adjPos.z,
          srcOffset.x,
          srcOffset.y,
          width,
          height,
          depthComponent.width,
          depthComponent.height,
          depthComponent.type,
          ((DepthComponentIntRetained) depthComponent).depthData);
    }
  }
  @Override
  boolean intersect(
      PickShape pickShape,
      PickInfo pickInfo,
      int flags,
      Point3d iPnt,
      GeometryRetained geom,
      int geomIndex) {
    Point3d pnts[] = new Point3d[2];
    double sdist[] = new double[1];
    double minDist = Double.MAX_VALUE;
    double x = 0, y = 0, z = 0;
    int scount, j, i = 0;
    int count = 0;
    int[] vtxIndexArr = new int[2];

    pnts[0] = new Point3d();
    pnts[1] = new Point3d();

    switch (pickShape.getPickType()) {
      case PickShape.PICKRAY:
        PickRay pickRay = (PickRay) pickShape;

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectLineAndRay(
                pnts[0], pnts[1], pickRay.origin, pickRay.direction, sdist, iPnt)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKSEGMENT:
        PickSegment pickSegment = (PickSegment) pickShape;
        Vector3d dir =
            new Vector3d(
                pickSegment.end.x - pickSegment.start.x,
                pickSegment.end.y - pickSegment.start.y,
                pickSegment.end.z - pickSegment.start.z);

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectLineAndRay(pnts[0], pnts[1], pickSegment.start, dir, sdist, iPnt)
                && (sdist[0] <= 1.0)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKBOUNDINGBOX:
        BoundingBox bbox = (BoundingBox) ((PickBounds) pickShape).bounds;

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectBoundingBox(pnts, bbox, sdist, iPnt)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKBOUNDINGSPHERE:
        BoundingSphere bsphere = (BoundingSphere) ((PickBounds) pickShape).bounds;

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectBoundingSphere(pnts, bsphere, sdist, iPnt)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKBOUNDINGPOLYTOPE:
        BoundingPolytope bpolytope = (BoundingPolytope) ((PickBounds) pickShape).bounds;

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectBoundingPolytope(pnts, bpolytope, sdist, iPnt)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKCYLINDER:
        PickCylinder pickCylinder = (PickCylinder) pickShape;

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectCylinder(pnts, pickCylinder, sdist, iPnt)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKCONE:
        PickCone pickCone = (PickCone) pickShape;

        while (i < stripIndexCounts.length) {
          vtxIndexArr[0] = indexCoord[count];
          getVertexData(indexCoord[count++], pnts[0]);
          scount = stripIndexCounts[i++];
          for (j = 1; j < scount; j++) {
            vtxIndexArr[1] = indexCoord[count];
            getVertexData(indexCoord[count++], pnts[1]);
            if (intersectCone(pnts, pickCone, sdist, iPnt)) {
              if (flags == 0) {
                return true;
              }
              if (sdist[0] < minDist) {
                minDist = sdist[0];
                x = iPnt.x;
                y = iPnt.y;
                z = iPnt.z;
                if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                  storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
                }
              }
              if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            pnts[0].set(pnts[1]);
            vtxIndexArr[0] = vtxIndexArr[1];
          }
        }
        break;
      case PickShape.PICKPOINT:
        // Should not happen since API already check for this
        throw new IllegalArgumentException(J3dI18N.getString("IndexedLineStripArrayRetained0"));
      default:
        throw new RuntimeException("PickShape not supported for intersection");
    }

    if (minDist < Double.MAX_VALUE) {
      iPnt.x = x;
      iPnt.y = y;
      iPnt.z = z;
      return true;
    }
    return false;
  }
  @Override
  synchronized void computeBoundingBox() {
    Point3d l = new Point3d();
    Point3d u = new Point3d();
    Vector3f location = new Vector3f(this.position);
    int i, k = 0, numTotal = 0;
    double width = 0, height = 0;
    Rectangle2D bounds;

    // Reset bounds data
    l.set(location);
    u.set(location);

    if (numChars != 0) {
      // Set loop counters based on path type
      if (path == Text3D.PATH_RIGHT || path == Text3D.PATH_UP) {
        k = 0;
        numTotal = numChars + 1;
      } else if (path == Text3D.PATH_LEFT || path == Text3D.PATH_DOWN) {
        k = 1;
        numTotal = numChars;
        // Reset bounds to bounding box if first character
        bounds = glyphVecs[0].getVisualBounds();
        u.x += bounds.getWidth();
        u.y += bounds.getHeight();
      }

      for (i = 1; i < numTotal; i++, k++) {
        width = glyphVecs[k].getLogicalBounds().getWidth();
        bounds = glyphVecs[k].getVisualBounds();
        // 'switch' could be outside loop with little hacking,
        width += charSpacing;
        height = bounds.getHeight();

        switch (this.path) {
          case Text3D.PATH_RIGHT:
            u.x += (width);
            if (u.y < (height + location.y)) {
              u.y = location.y + height;
            }
            break;
          case Text3D.PATH_LEFT:
            l.x -= (width);
            if (u.y < (height + location.y)) {
              u.y = location.y + height;
            }
            break;
          case Text3D.PATH_UP:
            u.y += height;
            if (u.x < (bounds.getWidth() + location.x)) {
              u.x = location.x + bounds.getWidth();
            }
            break;
          case Text3D.PATH_DOWN:
            l.y -= height;
            if (u.x < (bounds.getWidth() + location.x)) {
              u.x = location.x + bounds.getWidth();
            }
            break;
        }
      }

      // Handle string alignment. ALIGN_FIRST is handled by default
      if (alignment != Text3D.ALIGN_FIRST) {
        double cx = (u.x - l.x);
        double cy = (u.y - l.y);

        if (alignment == Text3D.ALIGN_CENTER) {
          cx *= .5;
          cy *= .5;
        }
        switch (path) {
          case Text3D.PATH_RIGHT:
            l.x -= cx;
            u.x -= cx;
            break;
          case Text3D.PATH_LEFT:
            l.x += cx;
            u.x += cx;
            break;
          case Text3D.PATH_UP:
            l.y -= cy;
            u.y -= cy;
            break;
          case Text3D.PATH_DOWN:
            l.y += cy;
            u.y += cy;
            break;
        }
      }
    }

    l.z = 0.0f;
    if ((font3D == null) || (font3D.fontExtrusion == null)) {
      u.z = l.z;
    } else {
      u.z = l.z + font3D.fontExtrusion.length;
    }
  }
  /**
   * Update per character transform based on Text3D location, per character size and path.
   *
   * <p>WARNING: Caller of this method must make sure SceneGraph is live, else exceptions may be
   * thrown.
   */
  final void updateTransformData() {
    int i, k = 0, numTotal = 0;
    double width = 0, height = 0;
    Vector3f location = new Vector3f(this.position);
    Rectangle2D bounds;

    // Reset bounds data
    lower.set(location);
    upper.set(location);

    charTransforms = new Transform3D[numChars];
    for (i = 0; i < numChars; i++) {
      charTransforms[i] = new Transform3D();
    }

    if (numChars != 0) {
      charTransforms[0].set(location);

      // Set loop counters based on path type
      if (path == Text3D.PATH_RIGHT || path == Text3D.PATH_UP) {
        k = 0;
        numTotal = numChars + 1;
      } else if (path == Text3D.PATH_LEFT || path == Text3D.PATH_DOWN) {
        k = 1;
        numTotal = numChars;
        // Reset bounds to bounding box if first character
        bounds = glyphVecs[0].getVisualBounds();
        upper.x += bounds.getWidth();
        upper.y += bounds.getHeight();
      }

      for (i = 1; i < numTotal; i++, k++) {
        width = glyphVecs[k].getLogicalBounds().getWidth();
        bounds = glyphVecs[k].getVisualBounds();
        // 'switch' could be outside loop with little hacking,
        width += charSpacing;
        height = bounds.getHeight();

        switch (this.path) {
          case Text3D.PATH_RIGHT:
            location.x += width;
            upper.x += (width);
            if (upper.y < (height + location.y)) {
              upper.y = location.y + height;
            }
            break;
          case Text3D.PATH_LEFT:
            location.x -= width;
            lower.x -= (width);
            if (upper.y < (height + location.y)) {
              upper.y = location.y + height;
            }
            break;
          case Text3D.PATH_UP:
            location.y += height;
            upper.y += height;
            if (upper.x < (bounds.getWidth() + location.x)) {
              upper.x = location.x + bounds.getWidth();
            }
            break;
          case Text3D.PATH_DOWN:
            location.y -= height;
            lower.y -= height;
            if (upper.x < (bounds.getWidth() + location.x)) {
              upper.x = location.x + bounds.getWidth();
            }
            break;
        }
        if (i < numChars) {
          charTransforms[i].set(location);
        }
      }

      // Handle string alignment. ALIGN_FIRST is handled by default
      if (alignment != Text3D.ALIGN_FIRST) {
        double cx = (upper.x - lower.x);
        double cy = (upper.y - lower.y);

        if (alignment == Text3D.ALIGN_CENTER) {
          cx *= .5;
          cy *= .5;
        }
        switch (path) {
          case Text3D.PATH_RIGHT:
            for (i = 0; i < numChars; i++) {
              charTransforms[i].mat[3] -= cx;
            }
            lower.x -= cx;
            upper.x -= cx;
            break;
          case Text3D.PATH_LEFT:
            for (i = 0; i < numChars; i++) {
              charTransforms[i].mat[3] += cx;
            }
            lower.x += cx;
            upper.x += cx;
            break;

          case Text3D.PATH_UP:
            for (i = 0; i < numChars; i++) {
              charTransforms[i].mat[7] -= cy;
            }
            lower.y -= cy;
            upper.y -= cy;
            break;
          case Text3D.PATH_DOWN:
            for (i = 0; i < numChars; i++) {
              charTransforms[i].mat[7] += cy;
            }
            lower.y += cy;
            upper.y += cy;
            break;
        }
      }
    }

    lower.z = 0.0f;
    if ((font3D == null) || (font3D.fontExtrusion == null)) {
      upper.z = lower.z;
    } else {
      upper.z = lower.z + font3D.fontExtrusion.length;
    }

    // update geoBounds
    getBoundingBox(geoBounds);
  }
  @Override
  boolean intersect(
      PickShape pickShape,
      PickInfo pickInfo,
      int flags,
      Point3d iPnt,
      GeometryRetained geom,
      int geomIndex) {
    Point3d pnts[] = new Point3d[3];
    double sdist[] = new double[1];
    double minDist = Double.MAX_VALUE;
    double x = 0, y = 0, z = 0;
    int[] vtxIndexArr = new int[3];

    // NVaidya
    // Bug 447: While loops below now traverse over all
    // elements in the valid index range from initialIndexIndex
    // to initialIndexInex + validIndexCount - 1
    int i = initialIndexIndex;
    int loopStopIndex = initialIndexIndex + validIndexCount;
    pnts[0] = new Point3d();
    pnts[1] = new Point3d();
    pnts[2] = new Point3d();

    switch (pickShape.getPickType()) {
      case PickShape.PICKRAY:
        PickRay pickRay = (PickRay) pickShape;

        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectRay(pnts, pickRay, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKSEGMENT:
        PickSegment pickSegment = (PickSegment) pickShape;
        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectSegment(pnts, pickSegment.start, pickSegment.end, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKBOUNDINGBOX:
        BoundingBox bbox = (BoundingBox) ((PickBounds) pickShape).bounds;

        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectBoundingBox(pnts, bbox, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKBOUNDINGSPHERE:
        BoundingSphere bsphere = (BoundingSphere) ((PickBounds) pickShape).bounds;

        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectBoundingSphere(pnts, bsphere, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKBOUNDINGPOLYTOPE:
        BoundingPolytope bpolytope = (BoundingPolytope) ((PickBounds) pickShape).bounds;

        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectBoundingPolytope(pnts, bpolytope, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKCYLINDER:
        PickCylinder pickCylinder = (PickCylinder) pickShape;
        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectCylinder(pnts, pickCylinder, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKCONE:
        PickCone pickCone = (PickCone) pickShape;

        while (i < loopStopIndex) {
          for (int j = 0; j < 3; j++) {
            vtxIndexArr[j] = indexCoord[i];
            getVertexData(indexCoord[i++], pnts[j]);
          }
          if (intersectCone(pnts, pickCone, sdist, iPnt)) {
            if (flags == 0) {
              return true;
            }
            if (sdist[0] < minDist) {
              minDist = sdist[0];
              x = iPnt.x;
              y = iPnt.y;
              z = iPnt.z;
              if ((flags & PickInfo.CLOSEST_GEOM_INFO) != 0) {
                storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
              }
            }
            if ((flags & PickInfo.ALL_GEOM_INFO) != 0) {
              storeInterestData(pickInfo, flags, geom, geomIndex, vtxIndexArr, iPnt, sdist[0]);
            }
          }
        }
        break;
      case PickShape.PICKPOINT:
        // Should not happen since API already check for this
        throw new IllegalArgumentException(J3dI18N.getString("IndexedTriangleArrayRetained0"));
      default:
        throw new RuntimeException("PickShape not supported for intersection");
    }

    if (minDist < Double.MAX_VALUE) {
      iPnt.x = x;
      iPnt.y = y;
      iPnt.z = z;
      return true;
    }
    return false;
  }