1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.javafx.experiments.exporters.javasource;
  33 
  34 import java.io.BufferedWriter;
  35 import java.io.File;
  36 import java.io.FileWriter;
  37 import java.io.IOException;
  38 import java.io.OutputStreamWriter;
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.Collections;
  42 import java.util.HashMap;
  43 import java.util.List;
  44 import java.util.Map;
  45 import javafx.animation.Interpolator;
  46 import javafx.animation.KeyFrame;
  47 import javafx.animation.KeyValue;
  48 import javafx.animation.Timeline;
  49 import javafx.beans.value.WritableValue;
  50 import javafx.scene.Group;
  51 import javafx.scene.Node;
  52 import javafx.scene.image.Image;
  53 import javafx.scene.paint.Color;
  54 import javafx.scene.paint.PhongMaterial;
  55 import javafx.scene.shape.MeshView;
  56 import javafx.scene.shape.TriangleMesh;
  57 import javafx.scene.transform.Rotate;
  58 import javafx.scene.transform.Scale;
  59 import javafx.scene.transform.Transform;
  60 import javafx.scene.transform.Translate;
  61 import javafx.util.Duration;
  62 import com.javafx.experiments.importers.Optimizer;
  63 import com.javafx.experiments.importers.maya.MayaImporter;
  64 import com.sun.javafx.animation.TickCalculation;
  65 import com.sun.scenario.animation.NumberTangentInterpolator;
  66 import com.sun.scenario.animation.SplineInterpolator;
  67 
  68 /**
  69  * A exporter for 3D Models and animations that creates a Java Source file.
  70  */
  71 public class JavaSourceExporter {
  72     private int nodeCount = 0;
  73     private int materialCount = 0;
  74     private int meshCount = 0;
  75     private int meshViewCount = 0;
  76     private int methodCount = 1;
  77     private int translateCount = 0;
  78     private int rotateCount = 0;
  79     private int scaleCount = 0;
  80     private Map<WritableValue,String> writableVarMap = new HashMap<>();
  81     private StringBuilder nodeCode = new StringBuilder();
  82     private StringBuilder timelineCode = new StringBuilder();
  83     private final boolean hasTimeline;
  84     private final String baseUrl;
  85     private final String className;
  86     private final String packageName;
  87     private final File outputFile;
  88 
  89     public JavaSourceExporter(String baseUrl, Node rootNode, Timeline timeline, File outputFile) {
  90         this(baseUrl, rootNode, timeline,computePackageName(baseUrl), outputFile);
  91     }
  92 
  93     public JavaSourceExporter(String baseUrl, Node rootNode, Timeline timeline, String packageName, File outputFile) {
  94         this.baseUrl =
  95                 (baseUrl.charAt(baseUrl.length()-1) == '/') ?
  96                         baseUrl.replaceAll("/+","/") :
  97                         baseUrl.replaceAll("/+","/") + '/';
  98         this.hasTimeline = timeline != null;
  99         this.className = outputFile.getName().substring(0,outputFile.getName().lastIndexOf('.'));
 100         this.packageName = packageName;
 101         this.outputFile = outputFile;
 102         process("        ",rootNode);
 103         if (hasTimeline) process("        ",timeline);
 104     }
 105 
 106     private static String computePackageName(String baseUrl) {
 107         // remove protocol from baseUrl
 108         System.out.println("JavaSourceExporter.computePackageName   baseUrl = " + baseUrl);
 109         baseUrl = baseUrl.replaceAll("^[a-z]+:/+","/");
 110         System.out.println("baseUrl = " + baseUrl);
 111         // try and work out package name from file
 112         StringBuilder packageName = new StringBuilder();
 113         String[] pathSegments = baseUrl.split(File.separatorChar == '\\'?"\\\\":"/");
 114         System.out.println("pathSegments = " + Arrays.toString(pathSegments));
 115         loop: for (int i = pathSegments.length-1; i >= 0; i -- ) {
 116             switch (pathSegments[i]){
 117                 case "com":
 118                 case "org":
 119                 case "net":
 120                     packageName.insert(0,pathSegments[i]);
 121                     break loop;
 122                 case "src":
 123                     packageName.deleteCharAt(0);
 124                     break loop;
 125                 case "main":
 126                     if ("src".equals(pathSegments[i-1])) {
 127                         packageName.deleteCharAt(0);
 128                         break loop;
 129                     }
 130                 default:
 131                     packageName.insert(0,'.'+pathSegments[i]);
 132                     break;
 133             }
 134             // if we get all way to root of file system then we have failed
 135             if (i==0) packageName = null;
 136         }
 137         System.out.println(" packageName = " + packageName);
 138         return packageName==null?null:packageName.toString();
 139     }
 140 
 141     public void export() {
 142         try {
 143             BufferedWriter out = new BufferedWriter(new FileWriter(outputFile));
 144 //            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
 145             StringBuilder nodeVars = new StringBuilder();
 146             nodeVars.append("    private static Node ");
 147             for (int i=0; i<nodeCount; i++) {
 148                 if (i!=0) nodeVars.append(',');
 149                 nodeVars.append("NODE_"+i);
 150             }
 151             nodeVars.append(";\n");
 152             nodeVars.append("    private static TriangleMesh ");
 153             for (int i=0; i<meshCount; i++) {
 154                 if (i!=0) nodeVars.append(',');
 155                 nodeVars.append("MESH_"+i);
 156             }
 157             nodeVars.append(";\n");
 158             if (translateCount > 0) {
 159                 nodeVars.append("    private static Translate ");
 160                 for (int i=0; i<translateCount; i++) {
 161                     if (i!=0) nodeVars.append(',');
 162                     nodeVars.append("TRANS_"+i);
 163                 }
 164                 nodeVars.append(";\n");
 165             }
 166             if (rotateCount > 0) {
 167                 nodeVars.append("    private static Rotate ");
 168                 for (int i=0; i<rotateCount; i++) {
 169                     if (i!=0) nodeVars.append(',');
 170                     nodeVars.append("ROT_"+i);
 171                 }
 172                 nodeVars.append(";\n");
 173             }
 174             if (scaleCount > 0) {
 175                 nodeVars.append("    private static Scale ");
 176                 for (int i=0; i<scaleCount; i++) {
 177                     if (i!=0) nodeVars.append(',');
 178                     nodeVars.append("SCALE_"+i);
 179                 }
 180                 nodeVars.append(";\n");
 181             }
 182             nodeVars.append("    public static final MeshView[] MESHVIEWS = new MeshView[]{ ");
 183             for (int i=0; i<meshViewCount; i++) {
 184                 if (i!=0) nodeVars.append(',');
 185                 nodeVars.append("new MeshView()");
 186             }
 187             nodeVars.append("};\n");
 188 //            nodeVars.append("    public static final MeshView ");
 189 //            for (int i=0; i<meshViewCount; i++) {
 190 //                if (i!=0) nodeVars.append(',');
 191 //                nodeVars.append("MESHVIEW_"+i+" = new MeshView()");
 192 //            }
 193 //            nodeVars.append(";\n");
 194 
 195             nodeVars.append("    public static final Node ROOT;\n");
 196             nodeVars.append("    public static final Map<String,MeshView> MESHVIEW_MAP;\n");
 197             if (hasTimeline) {
 198                 nodeVars.append("    public static final Timeline TIMELINE = new Timeline();\n");
 199             }
 200             StringBuilder methodCode = new StringBuilder();
 201             for (int m=0; m< methodCount;m++) {
 202                 methodCode.append("        method"+m+"();\n");
 203             }
 204 
 205             if (packageName != null) out.write(
 206                     "package "+packageName+";\n\n");
 207             out.write(
 208                     "import java.util.*;\n" +
 209                     "import javafx.util.Duration;\n" +
 210                     "import javafx.animation.*;\n" +
 211                     "import javafx.scene.*;\n" +
 212                     "import javafx.scene.paint.*;\n" +
 213                     "import javafx.scene.image.*;\n" +
 214                     "import javafx.scene.shape.*;\n" +
 215                     "import javafx.scene.transform.*;\n\n" +
 216                     "public class "+className+" {\n" +
 217                     nodeVars +
 218                     "    // ======== NODE CODE ===============\n" +
 219                     "    private static void method0(){\n" +
 220                     nodeCode +
 221                     "    }\n" +
 222                     "    static {\n" +
 223                     methodCode +
 224                     "        // ======== TIMELINE CODE ===============\n" +
 225                     timelineCode +
 226                     "        // ======== SET PUBLIC VARS ===============\n" +
 227                     "        Map<String,MeshView> meshViewMap = new HashMap<String,MeshView>();\n" +
 228                     "        for (MeshView meshView: MESHVIEWS) meshViewMap.put(meshView.getId(),meshView);\n" +
 229                     "        MESHVIEW_MAP = Collections.unmodifiableMap(meshViewMap);\n" +
 230                     "        ROOT = NODE_0;\n" +
 231                     "    }\n" +
 232                     "}\n");
 233 
 234             out.flush();
 235             out.close();
 236         } catch (IOException e) {
 237             e.printStackTrace();
 238         }
 239     }
 240 
 241     private int process(String indent, Node node) {
 242         if (node instanceof MeshView) {
 243             return process(indent,(MeshView)node);
 244         } else if (node instanceof Group) {
 245             return process(indent,(Group)node);
 246         } else {
 247             throw new UnsupportedOperationException("Found unknown node type: "+node.getClass().getName());
 248         }
 249     }
 250 
 251     private int process(String indent, MeshView node) {
 252         final int index = nodeCount ++;
 253         final String varName = "NODE_"+index;
 254         final int meshViewIndex = meshViewCount ++;
 255         final String meshViewVarName = "MESHVIEWS["+meshViewIndex+"]";
 256 //        nodeCode.append(indent+meshViewVarName+" = new MeshView();\n");
 257         nodeCode.append(indent+varName+" = "+meshViewVarName+";\n");
 258         if (node.getId() != null) nodeCode.append(indent+meshViewVarName+".setId(\""+node.getId()+"\");\n");
 259         nodeCode.append(indent+meshViewVarName+".setCullFace(CullFace."+node.getCullFace().name()+");\n");
 260         processNodeTransforms(indent,meshViewVarName,node);
 261         process(indent,meshViewVarName,(PhongMaterial)node.getMaterial());
 262         process(indent,meshViewVarName,(TriangleMesh)node.getMesh());
 263         return index;
 264     }
 265 
 266     private void processNodeTransforms(String indent, String varName, Node node) {
 267         if (node.getTranslateX() != 0) nodeCode.append(indent+varName+".setTranslateX("+node.getTranslateX()+");\n");
 268         if (node.getTranslateY() != 0) nodeCode.append(indent+varName+".setTranslateY("+node.getTranslateY()+");\n");
 269         if (node.getTranslateZ() != 0) nodeCode.append(indent+varName+".setTranslateZ("+node.getTranslateZ()+");\n");
 270         if (node.getRotate() != 0) nodeCode.append(indent+varName+".setRotate("+node.getRotate()+");\n");
 271         if (!node.getTransforms().isEmpty()) {
 272             nodeCode.append(indent+varName+".getTransforms().addAll(\n");
 273             for (int i=0; i< node.getTransforms().size(); i++) {
 274                 if (i!=0) nodeCode.append(",\n");
 275                 Transform transform = node.getTransforms().get(i);
 276                 if (transform instanceof Translate) {
 277                     Translate t = (Translate)transform;
 278                     nodeCode.append(indent+"    "+storeTransform(t)+" = new Translate("+t.getX()+","+t.getY()+","+t.getZ()+")");
 279                 } else if (transform instanceof Scale) {
 280                     Scale s = (Scale)transform;
 281                     nodeCode.append(indent+"    "+storeTransform(s)+" = new Scale("+s.getX()+","+s.getY()+","+s.getZ()+","+s.getPivotX()+","+s.getPivotY()+","+s.getPivotZ()+")");
 282                 } else if (transform instanceof Rotate) {
 283                     Rotate r = (Rotate)transform;
 284                     nodeCode.append(indent+"    "+storeTransform(r)+" = new Rotate("+r.getAngle()+","+r.getPivotX()+","+r.getPivotY()+","+r.getPivotZ()+",");
 285                     if (r.getAxis() == Rotate.X_AXIS) {
 286                         nodeCode.append("Rotate.X_AXIS");
 287                     } else if (r.getAxis() == Rotate.Y_AXIS) {
 288                         nodeCode.append("Rotate.Y_AXIS");
 289                     } else if (r.getAxis() == Rotate.Z_AXIS) {
 290                         nodeCode.append("Rotate.Z_AXIS");
 291                     }
 292                     nodeCode.append(")");
 293                 } else {
 294                     throw new UnsupportedOperationException("Unknown Transform Type: "+transform.getClass());
 295                 }
 296 
 297             }
 298             nodeCode.append("\n"+indent+");\n");
 299         }
 300     }
 301 
 302     private String storeTransform(Transform transform) {
 303         String varName;
 304         if (transform instanceof Translate) {
 305             final int index = translateCount ++;
 306             varName = "TRANS_"+index;
 307             Translate t = (Translate)transform;
 308             writableVarMap.put(t.xProperty(),varName+".xProperty()");
 309             writableVarMap.put(t.yProperty(),varName+".yProperty()");
 310             writableVarMap.put(t.zProperty(),varName+".zProperty()");
 311         } else if (transform instanceof Scale) {
 312             final int index = scaleCount ++;
 313             varName = "SCALE_"+index;
 314             Scale s = (Scale)transform;
 315             writableVarMap.put(s.xProperty(),varName+".xProperty()");
 316             writableVarMap.put(s.yProperty(),varName+".yProperty()");
 317             writableVarMap.put(s.zProperty(),varName+".zProperty()");
 318             writableVarMap.put(s.pivotXProperty(),varName+".pivotXProperty()");
 319             writableVarMap.put(s.pivotYProperty(),varName+".pivotYProperty()");
 320             writableVarMap.put(s.pivotZProperty(),varName+".pivotZProperty()");
 321         } else if (transform instanceof Rotate) {
 322             final int index = rotateCount ++;
 323             varName = "ROT_"+index;
 324             Rotate r = (Rotate)transform;
 325             writableVarMap.put(r.angleProperty(),varName+".angleProperty()");
 326         } else {
 327             throw new UnsupportedOperationException("Unknown Transform Type: "+transform.getClass());
 328         }
 329         return varName;
 330     }
 331 
 332     private int process(String indent, Group node) {
 333         final int index = nodeCount ++;
 334         final String varName = "NODE_"+index;
 335         List<Integer> childIndex = new ArrayList<>();
 336         for (int i = 0; i < node.getChildren().size(); i++) {
 337             Node child = node.getChildren().get(i);
 338             childIndex.add(
 339                     process(indent,child));
 340         }
 341         nodeCode.append(indent+varName+" = new Group(");
 342         for (int i = 0; i < childIndex.size(); i++) {
 343             if (i!=0) nodeCode.append(',');
 344             nodeCode.append("NODE_"+childIndex.get(i));
 345         }
 346         nodeCode.append(");\n");
 347         processNodeTransforms(indent, varName, node);
 348         nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
 349         return index;
 350     }
 351 
 352     private void process(String indent, String varName, TriangleMesh mesh) {
 353         final int index = meshCount ++;
 354         final String meshName = "MESH_"+index;
 355 
 356         nodeCode.append(indent+meshName+" = new TriangleMesh();\n");
 357         nodeCode.append(indent+varName+".setMesh("+meshName+");\n");
 358 
 359         nodeCode.append(indent+meshName+".getPoints().ensureCapacity("+mesh.getPoints().size()+");\n");
 360         nodeCode.append(indent + meshName + ".getPoints().addAll(");
 361         for (int i = 0; i < mesh.getPoints().size(); i++) {
 362             if (i!=0) nodeCode.append(',');
 363             nodeCode.append(mesh.getPoints().get(i)+"f");
 364         }
 365         nodeCode.append(");\n");
 366         nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
 367         nodeCode.append(indent+meshName+".getTexCoords().ensureCapacity("+mesh.getTexCoords().size()+");\n");
 368         nodeCode.append(indent + meshName + ".getTexCoords().addAll(");
 369         for (int i = 0; i < mesh.getTexCoords().size(); i++) {
 370             if (i!=0) nodeCode.append(',');
 371             nodeCode.append(mesh.getTexCoords().get(i)+"f");
 372         }
 373         nodeCode.append(");\n");
 374         nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
 375         nodeCode.append(indent+meshName+".getFaces().ensureCapacity("+mesh.getFaces().size()+");\n");
 376         nodeCode.append(indent + meshName + ".getFaces().addAll(");
 377         for (int i = 0; i < mesh.getFaces().size(); i++) {
 378             if ((i%5000) == 0 && i > 0) {
 379                 nodeCode.append(");\n");
 380                 nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
 381                 nodeCode.append(indent+meshName+".getFaces().addAll(");
 382             } else if (i!=0) {
 383                 nodeCode.append(',');
 384             }
 385             nodeCode.append(mesh.getFaces().get(i));
 386         }
 387         nodeCode.append(");\n");
 388         nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
 389         nodeCode.append(indent+meshName+".getFaceSmoothingGroups().ensureCapacity("+mesh.getFaceSmoothingGroups().size()+");\n");
 390         nodeCode.append(indent+meshName+".getFaceSmoothingGroups().addAll(");
 391         for (int i = 0; i < mesh.getFaceSmoothingGroups().size(); i++) {
 392             if (i!=0) nodeCode.append(',');
 393             nodeCode.append(mesh.getFaceSmoothingGroups().get(i));
 394         }
 395         nodeCode.append(");\n");
 396 
 397 //        nodeCode.append(indent+varName+".setMesh("+process((TriangleMesh)node.getMesh())+");\n");
 398     }
 399 
 400     private void process(String indent, String varName, PhongMaterial material) {
 401         final int index = materialCount ++;
 402         final String materialName = "MATERIAL_"+index;
 403 
 404         nodeCode.append(indent + "PhongMaterial " + materialName + " = new PhongMaterial();\n");
 405         nodeCode.append(indent + materialName + ".setDiffuseColor(" + toCode(material.getDiffuseColor()) + ");\n");
 406         String specColor = toCode(material.getSpecularColor());
 407         if (specColor!=null) nodeCode.append(indent + materialName + ".setSpecularColor(" + specColor + ");\n");
 408         nodeCode.append(indent + materialName + ".setSpecularPower("+material.getSpecularPower()+");\n");
 409         if (material.getDiffuseMap() != null) {
 410             nodeCode.append(indent + "try {\n");
 411             nodeCode.append(indent + "    " + materialName + ".setDiffuseMap("+toString(material.getDiffuseMap())+");\n");
 412             nodeCode.append(indent + "} catch (NullPointerException npe) {\n");
 413             nodeCode.append(indent + "    System.err.println(\"Could not load texture resource ["+material.getDiffuseMap().impl_getUrl()+"]\");\n");
 414             nodeCode.append(indent + "}\n");
 415         }
 416         if (material.getBumpMap() != null) {
 417             nodeCode.append(indent + materialName + ".setBumpMap("+toString(material.getBumpMap())+");\n");
 418         }
 419         if (material.getSpecularMap() != null) {
 420             nodeCode.append(indent + materialName + ".setSpecularMap()("+toString(material.getSpecularMap())+");\n");
 421         }
 422         if (material.getSelfIlluminationMap() != null) {
 423             nodeCode.append(indent + materialName + ".setSelfIlluminationMap()("+toString(material.getSelfIlluminationMap())+");\n");
 424         }
 425         nodeCode.append(indent+varName+".setMaterial("+materialName+");\n");
 426     }
 427 
 428     private String toString(Image image) {
 429         String url = image.impl_getUrl();
 430         if (url.startsWith(baseUrl)) {
 431             return  "new Image("+className+".class.getResource(\""+url.substring(baseUrl.length())+"\").toExternalForm())";
 432         } else {
 433             return "new Image(\""+url+"\")";
 434         }
 435     }
 436 
 437     private void process(String indent, Timeline timeline) {
 438         int count = 0;
 439         for (KeyFrame keyFrame: timeline.getKeyFrames()) {
 440             if (keyFrame.getValues().isEmpty()) continue;
 441             nodeCode.append(indent+"TIMELINE.getKeyFrames().add(new KeyFrame(Duration.millis("+keyFrame.getTime().toMillis()+"d),\n");
 442             boolean firstKeyValue = true;
 443             for (KeyValue keyValue: keyFrame.getValues()) {
 444                 if (firstKeyValue) {
 445                     firstKeyValue = false;
 446                 } else {
 447                     nodeCode.append(",\n");
 448                 }
 449                 String var = writableVarMap.get(keyValue.getTarget());
 450                 if (var == null) System.err.println("Failed to find writable value in map for : "+keyValue.getTarget());
 451                 nodeCode.append(indent+"    new KeyValue("+var+","+keyValue.getEndValue()+","+toString(keyValue.getInterpolator())+")");
 452             }
 453             nodeCode.append("\n"+indent+"));\n");
 454 
 455             if (count > 0 && ((count % 10) == 0)) {
 456                 nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
 457             }
 458             count ++;
 459         }
 460     }
 461 
 462     private String toString(Interpolator interpolator) {
 463 //        if (interpolator == Interpolator.DISCRETE || true) {
 464         if (interpolator == Interpolator.DISCRETE) {
 465             return "Interpolator.DISCRETE";
 466         } else if (interpolator == Interpolator.EASE_BOTH) {
 467             return "Interpolator.EASE_BOTH";
 468         } else if (interpolator == Interpolator.EASE_IN) {
 469             return "Interpolator.EASE_IN";
 470         } else if (interpolator == Interpolator.EASE_OUT) {
 471             return "Interpolator.EASE_OUT";
 472         } else if (interpolator == Interpolator.LINEAR) {
 473             return "Interpolator.LINEAR";
 474         } else if (interpolator instanceof SplineInterpolator) {
 475             SplineInterpolator si = (SplineInterpolator)interpolator;
 476             return "Interpolator.SPLINE("+si.getX1()+"d,"+si.getY1()+"d,"+si.getX2()+"d,"+si.getY2()+"d)";
 477         } else if (interpolator instanceof NumberTangentInterpolator) {
 478             NumberTangentInterpolator ti = (NumberTangentInterpolator)interpolator;
 479             return "Interpolator.TANGENT(Duration.millis("+ TickCalculation.toMillis((long)ti.getInTicks())+"d),"+ti.getInValue()
 480                     +"d,Duration.millis("+TickCalculation.toMillis((long)ti.getInTicks())+"d),"+ti.getOutValue()+"d)";
 481         } else {
 482             throw new UnsupportedOperationException("Unknown Interpolator type: "+interpolator.getClass());
 483         }
 484     }
 485 
 486     private String toCode(Color color) {
 487         return color == null ? null : "new Color("+color.getRed()+","+color.getGreen()+","+color.getBlue()+","+color.getOpacity()+")";
 488     }
 489 }