changeset 4011:b1d681005b05 8.0-b95

Merge
author ngthomas
date Wed, 19 Jun 2013 13:57:16 -0700
parents d5f576e93c8d 8c4c7f85a7c7
children 71bbec00a794 646fda36947d b8d829dbef43
files deploy/packager/native/windows/WinLauncher.cpp glass/glass-lib-lens/src/android/Main.c glass/glass-lib-lens/src/android/Main.h gradleBuildSrc/src/main/groovy/com/sun/javafx/build/CCTask.groovy gradleBuildSrc/src/main/groovy/com/sun/javafx/build/CompileHLSLTask.groovy gradleBuildSrc/src/main/groovy/com/sun/javafx/build/CompileResourceTask.groovy gradleBuildSrc/src/main/groovy/com/sun/javafx/build/JavaHeaderTask.groovy gradleBuildSrc/src/main/groovy/com/sun/javafx/build/LinkTask.groovy gradleBuildSrc/src/main/groovy/com/sun/javafx/build/NativeCompileTask.groovy javafx-builders/test/unit/com/sun/javafx/test/PropertiesTestBase.java javafx-builders/test/unit/com/sun/javafx/test/PropertyReference.java javafx-builders/test/unit/com/sun/javafx/test/binding/ReflectionHelper.java javafx-builders/test/unit/javafx/scene/Node_properties_Test.java javafx-builders/test/unit/javafx/scene/Scene_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/Blend_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/Bloom_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/BoxBlur_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/ColorAdjust_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/ColorInput_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/DisplacementMap_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/DistantLight_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/DropShadow_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/FloatMap_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/GaussianBlur_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/Glow_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/ImageInput_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/InnerShadow_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/Lighting_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/MotionBlur_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/PerspectiveTransform_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/PointLight_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/Reflection_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/SepiaTone_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/Shadow_properties_Test.java javafx-builders/test/unit/javafx/scene/effect/SpotLight_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/ArcTo_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Arc_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Circle_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/CubicCurveTo_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/CubicCurve_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Ellipse_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/HLineTo_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/LineTo_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Line_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/MoveTo_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Path_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/QuadCurveTo_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/QuadCurve_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Rectangle_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/SVGPath_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/Shape_properties_Test.java javafx-builders/test/unit/javafx/scene/shape/VLineTo_properties_Test.java javafx-builders/test/unit/javafx/scene/transform/Transform_properties_Test.java javafx-fxml/.hgignore javafx-ui-quantum/src/com/sun/javafx/tk/quantum/EmbeddedPainter.java prism-common/src/com/sun/prism/render/CompletionListener.java prism-common/src/com/sun/prism/render/RenderJob.java prism-common/src/com/sun/prism/render/ToolkitInterface.java
diffstat 1220 files changed, 24845 insertions(+), 13224 deletions(-) [+]
line wrap: on
line diff
--- a/.classpath	Mon Jun 17 18:00:06 2013 -0700
+++ b/.classpath	Wed Jun 19 13:57:16 2013 -0700
@@ -1,188 +1,197 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
-    <classpathentry kind="src" path="apps/experiments/3DViewer/src/main/java"/>
-    <classpathentry kind="src" path="apps/experiments/3DViewer/src/test/java"/>
-    <classpathentry kind="src" path="apps/experiments/3DViewer/src/test/resources"/>
-    <!--
-    <classpathentry kind="src" path="apps/experiments/ConferenceScheduleApp/src"/>
-    <classpathentry kind="src" path="apps/experiments/ModenaTest/test"/>
-    <classpathentry kind="src" path="apps/experiments/Modena/src"/>
-    -->
-    <classpathentry kind="src" path="apps/samples/Ensemble8/src/app"/>
-    <classpathentry kind="src" path="apps/samples/Ensemble8/src/compiletime"/>
-    <classpathentry kind="src" path="apps/samples/Ensemble8/src/generated"/>
-    <classpathentry kind="src" path="apps/samples/Ensemble8/src/samples"/>
-    <classpathentry kind="lib" path="apps/samples/Ensemble8/lib/lucene-core-3.2.0.jar"/>
-    <classpathentry kind="lib" path="apps/samples/Ensemble8/lib/lucene-grouping-3.2.0.jar"/>
-
-    <classpathentry kind="src" path="decora-compiler/build/gensrc"/>
-    <classpathentry kind="src" path="decora-compiler/src"/>
-    <classpathentry kind="src" path="decora-d3d/src"/>
-    <classpathentry kind="src" path="decora-d3d/build/gensrc"/>
-    <classpathentry kind="src" path="decora-es2/src"/>
-    <classpathentry kind="src" path="decora-es2/build/gensrc"/>
-    <classpathentry kind="src" path="decora-jsw/build/gensrc"/>
-    <classpathentry kind="src" path="decora-jsw/src"/>
-    <classpathentry kind="src" path="decora-prism-ps/build/gensrc"/>
-    <classpathentry kind="src" path="decora-prism-ps/src"/>
-    <classpathentry kind="src" path="decora-prism-sw/src"/>
-    <classpathentry kind="src" path="decora-prism/src"/>
-    <classpathentry kind="src" path="decora-runtime/src"/>
-    <classpathentry kind="src" path="decora-runtime/generator"/>
-    <classpathentry kind="src" path="decora-runtime/test"/>
-    <classpathentry kind="src" path="decora-sse/src"/>
-    <classpathentry kind="src" path="decora-sse/build/gensrc"/>
-
-    <classpathentry kind="src" path="deploy/javafx-deploy/src"/>
-    <classpathentry kind="src" path="deploy/javafx-launcher/src"/>
-    <classpathentry kind="src" path="deploy/packager/src"/>
-    <classpathentry kind="src" path="deploy/packager/test"/>
-
-    <classpathentry kind="src" path="glass/glass/src"/>
-
-    <classpathentry kind="src" path="javafx-anim/src"/>
-    <classpathentry kind="src" path="javafx-anim/test/unit"/>
-    <!--
-    <classpathentry kind="src" path="javafx-annotation-processor/src"/>
-    -->
-    <classpathentry kind="src" path="javafx-beans/src"/>
-    <classpathentry kind="src" path="javafx-beans/test"/>
-    <classpathentry kind="src" path="javafx-beans-dt/src"/>
-    <classpathentry kind="src" path="javafx-beans-dt/test"/>
-
-    <!--
-    <classpathentry kind="src" path="javafx-builders/test/unit"/>
-    <classpathentry kind="src" path="javafx-builders/src"/>
-    -->
-    <classpathentry kind="lib" exported="true" path="javafx-builders/dist/javafx-builders.jar">
-        <attributes>
-            <attribute name="optional" value="true"/>
-        </attributes>
-    </classpathentry>
-    
-    <classpathentry kind="src" path="javafx-concurrent/src"/>
-    <classpathentry kind="src" path="javafx-concurrent/test"/>
-    <classpathentry kind="src" path="javafx-common/test/unit"/>
-    <classpathentry kind="src" path="javafx-common/build/gensrc/classes"/>
-    <classpathentry kind="src" path="javafx-common/src"/>
-    <classpathentry kind="src" path="javafx-designtime/src"/>
-    <!--
-    <classpathentry kind="src" path="javafx-designtime/test"/>
-    -->
-    <!-- Temporarily excluding SwingNode as it requires Java 8 -->
-    <!--
-    <classpathentry kind="src" excluding="javafx/embed/swing/SwingNode.java" path="javafx-embed-swing/src"/>
-    -->
-    <classpathentry kind="src" path="javafx-embed-swing/src"/>
-    <classpathentry kind="src" path="javafx-embed-swt/src"/>
-    <classpathentry kind="src" path="javafx-fxml/src"/>
-    <classpathentry kind="src" path="javafx-fxml/test"/>
-    <classpathentry kind="src" path="javafx-geom/src"/>
-    <classpathentry kind="src" path="javafx-geom/test"/>	
-    <classpathentry kind="src" path="javafx-iio/src"/>
-    <classpathentry kind="src" path="javafx-iio/test"/>
-    <classpathentry kind="src" path="javafx-logging/src"/>
-    <classpathentry kind="src" path="javafx-sg-common/src"/>
-    <classpathentry kind="src" path="javafx-sg-common/test"/>
-    <classpathentry kind="src" path="javafx-sg-prism/src"/>
-    <classpathentry kind="src" path="javafx-sg-prism/test"/>
-    <classpathentry kind="src" path="javafx-ui-charts/src"/>
-    <classpathentry kind="src" path="javafx-ui-charts/test"/>
-    <classpathentry kind="src" path="javafx-ui-common/src"/>
-    <!--
-    <classpathentry kind="src" path="javafx-ui-common/test/unit"/>
-    -->
-    <classpathentry kind="src" path="javafx-ui-controls/src"/>
-    <classpathentry kind="src" path="javafx-ui-controls/test"/>
-    <classpathentry kind="src" path="javafx-ui-quantum/src"/>
-    <classpathentry kind="src" path="javafx-util-converter/src"/>
-    <classpathentry kind="src" path="javafx-util-converter/test"/>
-
-    <classpathentry kind="src" path="pisces/src"/>
-
-    <classpathentry kind="src" path="prism-common/src"/>
-    <classpathentry kind="src" path="prism-common/test"/>
-	<classpathentry kind="src" path="prism-d3d/src"/>
-	<classpathentry kind="src" path="prism-d3d/build/gensrc">
-		<attributes>
-			<attribute name="optional" value="true"/>
-		</attributes>
-	</classpathentry>
-	<classpathentry kind="src" path="prism-es2/src"/>
-	<classpathentry kind="src" path="prism-es2-eglfb/src"/>
-	<classpathentry kind="src" path="prism-es2-eglx11/src"/>
-	<!--
-	<classpathentry kind="src" path="prism-es2-ios/src"/>
-	-->
-	<classpathentry kind="src" path="prism-es2-mac/src"/>
-	<classpathentry kind="src" path="prism-es2-win/src"/>
-	<classpathentry kind="src" path="prism-es2-x11/src"/>
-	<classpathentry kind="src" path="prism-es2/build/gensrc">
-		<attributes>
-			<attribute name="optional" value="true"/>
-		</attributes>
-	</classpathentry>
-    <classpathentry kind="src" path="prism-j2d/src"/>
-    <classpathentry kind="src" path="prism-sw/src"/>
-    <classpathentry kind="src" path="prism-null/src"/>
-    <classpathentry kind="src" path="prism-ps/src"/>
-    <classpathentry kind="src" path="prism-ps/build/gensrc"/>
-	<classpathentry kind="src" path="prism-ps/test"/>
-	<!--
-	<classpathentry kind="src" path="prism-ps/shadergen"/>
-	-->
-    <classpathentry kind="src" path="prism-util/src"/>
-
-    <classpathentry kind="src" path="test-stub-toolkit/src"/>
-	<classpathentry kind="src" path="tests/app-lifecycle/ClassLoaderApp/src"/>
-	<classpathentry kind="src" path="tests/app-lifecycle/ClassLoaderTest/test"/>
-	<classpathentry kind="src" path="tests/app-lifecycle/LauncherTests/test"/>
-	<classpathentry kind="src" path="tests/app-lifecycle/LifeCycleTests/test"/>
-	<classpathentry kind="src" path="tests/glass/ExceptionHandler/test"/>
-	<classpathentry kind="src" path="tests/graphics/MaterialTests/test"/>
-	<classpathentry kind="src" path="tests/graphics/SnapshotTests/test"/>
-	<classpathentry kind="src" path="tests/graphics/SwingInterop/test"/>
-	<classpathentry kind="src" path="tests/manual/printing"/>
-	<classpathentry kind="src" path="tests/quantum/CloseWindow/test"/>
-	<classpathentry kind="src" path="tests/quantum/WindowSceneInitDispose/test"/>
-
-	<classpathentry kind="src" path="webview/src"/>
-	<!--
-	<classpathentry kind="src" path="webview/test"/>
-	-->
-	<classpathentry combineaccessrules="false" kind="src" path="webview/native/WebKitBuild/Release/WebCore/generated/java">
-        <attributes>
-            <attribute name="optional" value="true"/>
-        </attributes>
-    </classpathentry>
-    <classpathentry kind="lib" path="../caches/sdk/build/lib/desktop/webview.jar">
-        <attributes>
-            <attribute name="optional" value="true"/>
-        </attributes>
-    </classpathentry>
-
-    <classpathentry kind="src" path="/rt-closed"/>
-	<classpathentry combineaccessrules="false" kind="src" path="/media">
-		<attributes>
-			<attribute name="optional" value="true"/>
-		</attributes>
-	</classpathentry>
-	<classpathentry kind="lib" path="../caches/sdk/build/lib/desktop/jfxmedia.jar">
-		<attributes>
-			<attribute name="optional" value="true"/>
-		</attributes>
-	</classpathentry>
-
-    <classpathentry kind="src" path="/org.eclipse.swt">
-        <attributes>
-            <attribute name="optional" value="true"/>
-        </attributes>
-    </classpathentry>
-    <classpathentry kind="lib" path="../import/swt-3.7.1/swt-debug.jar"/>
-    <classpathentry kind="output" path="bin"/>
-    
-    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
-    <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
-    <classpathentry kind="lib" path="../import/antlr-3.1.3/antlr-3.1.3/lib/antlr-3.1.3.jar"/>
-    <classpathentry kind="lib" path="../import/findbugs-2.0.1/findbugs-2.0.1/lib/ant.jar"/>
-</classpath>
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="apps/experiments/3DViewer/src/main/java"/>
+	<classpathentry kind="src" path="apps/experiments/3DViewer/src/test/java"/>
+	<classpathentry kind="src" path="apps/experiments/3DViewer/src/test/resources"/>
+	<!--
+	<classpathentry kind="src" path="apps/experiments/ConferenceScheduleApp/src"/>
+	<classpathentry kind="src" path="apps/experiments/ModenaTest/test"/>
+	-->
+	<classpathentry kind="src" path="apps/experiments/Modena/src"/>
+	<classpathentry kind="src" path="apps/samples/Ensemble8/src/app"/>
+	<classpathentry kind="src" path="apps/samples/Ensemble8/src/compiletime"/>
+	<classpathentry kind="src" path="apps/samples/Ensemble8/src/generated"/>
+	<classpathentry kind="src" path="apps/samples/Ensemble8/src/samples"/>
+	<classpathentry kind="lib" path="apps/samples/Ensemble8/lib/lucene-core-3.2.0.jar"/>
+	<classpathentry kind="lib" path="apps/samples/Ensemble8/lib/lucene-grouping-3.2.0.jar"/>
+
+	<classpathentry kind="src" path="decora-compiler/build/gensrc"/>
+	<classpathentry kind="src" path="decora-compiler/src"/>
+	<classpathentry kind="src" path="decora-d3d/src"/>
+	<classpathentry kind="src" path="decora-d3d/build/gensrc"/>
+	<classpathentry kind="src" path="decora-es2/src"/>
+	<classpathentry kind="src" path="decora-es2/build/gensrc"/>
+	<classpathentry kind="src" path="decora-jsw/build/gensrc"/>
+	<classpathentry kind="src" path="decora-jsw/src"/>
+	<classpathentry kind="src" path="decora-prism-ps/build/gensrc"/>
+	<classpathentry kind="src" path="decora-prism-ps/src"/>
+	<classpathentry kind="src" path="decora-prism-sw/src"/>
+	<classpathentry kind="src" path="decora-prism/src"/>
+	<classpathentry kind="src" path="decora-runtime/src"/>
+	<classpathentry kind="src" path="decora-runtime/generator"/>
+	<classpathentry kind="src" path="decora-runtime/test"/>
+	<classpathentry kind="src" path="decora-sse/src"/>
+	<classpathentry kind="src" path="decora-sse/build/gensrc"/>
+
+	<classpathentry kind="src" path="deploy/javafx-deploy/src"/>
+	<classpathentry kind="src" path="deploy/javafx-launcher/src"/>
+	<classpathentry kind="src" path="deploy/packager/src"/>
+	<classpathentry kind="src" path="deploy/packager/test"/>
+
+	<classpathentry kind="src" path="glass/glass/src"/>
+
+	<classpathentry kind="src" path="javafx-accessible/src"/>
+	<classpathentry kind="src" path="javafx-anim/src"/>
+	<classpathentry kind="src" path="javafx-anim/test/unit"/>
+	<!--
+	<classpathentry kind="src" path="javafx-annotation-processor/src"/>
+	-->
+	<classpathentry kind="src" path="javafx-beans/src"/>
+	<classpathentry kind="src" path="javafx-beans/test"/>
+	<classpathentry kind="src" path="javafx-beans-dt/src"/>
+	<classpathentry kind="src" path="javafx-beans-dt/test"/>
+
+	<!-- 
+	Builders are deprecated and do notbuild clean on new versions of JDK.
+	Using the library instead of the source. 
+	-->
+	<!--
+	<classpathentry kind="src" path="javafx-builders/test/unit"/>
+	<classpathentry kind="src" path="javafx-builders/src"/>
+	-->
+	<classpathentry kind="lib" exported="true" path="javafx-builders/dist/javafx-builders.jar" sourcepath="javafx-builders/src">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+
+	<classpathentry kind="src" path="javafx-concurrent/src"/>
+	<classpathentry kind="src" path="javafx-concurrent/test"/>
+	<classpathentry kind="src" path="javafx-common/test/unit"/>
+	<classpathentry kind="src" path="javafx-common/build/gensrc/classes"/>
+	<classpathentry kind="src" path="javafx-common/src"/>
+	<classpathentry kind="src" path="javafx-designtime/src"/>
+	<!--
+	<classpathentry kind="src" path="javafx-designtime/test"/>
+	-->
+	<!-- Temporarily excluding SwingNode as it requires Java 8 -->
+	<!--
+	<classpathentry kind="src" excluding="javafx/embed/swing/SwingNode.java" path="javafx-embed-swing/src"/>
+	-->
+	<classpathentry kind="src" path="javafx-embed-swing/src"/>
+	<classpathentry kind="src" path="javafx-embed-swt/src"/>
+	<classpathentry kind="src" path="javafx-fxml/src"/>
+	<classpathentry kind="src" path="javafx-fxml/test"/>
+	<classpathentry kind="src" path="javafx-geom/src"/>
+	<classpathentry kind="src" path="javafx-geom/test"/>	
+	<classpathentry kind="src" path="javafx-iio/src"/>
+	<classpathentry kind="src" path="javafx-iio/test"/>
+	<classpathentry kind="src" path="javafx-logging/src"/>
+	<classpathentry kind="src" path="javafx-sg-common/src"/>
+	<classpathentry kind="src" path="javafx-sg-common/test"/>
+	<classpathentry kind="src" path="javafx-sg-prism/src"/>
+	<classpathentry kind="src" path="javafx-sg-prism/test"/>
+	<classpathentry kind="src" path="javafx-ui-charts/src"/>
+	<classpathentry kind="src" path="javafx-ui-charts/test"/>
+	<classpathentry kind="src" path="javafx-ui-common/src"/>
+	<!--
+	<classpathentry kind="src" path="javafx-ui-common/test/unit"/>
+	-->
+	<classpathentry kind="src" path="javafx-ui-controls/src"/>
+	<classpathentry kind="src" path="javafx-ui-controls/test"/>
+	<classpathentry kind="src" path="javafx-ui-quantum/src"/>
+	<classpathentry kind="src" path="javafx-util-converter/src"/>
+	<classpathentry kind="src" path="javafx-util-converter/test"/>
+
+	<classpathentry kind="src" path="pisces/src"/>
+
+	<classpathentry kind="src" path="prism-common/src"/>
+	<classpathentry kind="src" path="prism-common/test"/>
+	<classpathentry kind="src" path="prism-d3d/src"/>
+	<classpathentry kind="src" path="prism-d3d/build/gensrc">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" path="prism-es2/src"/>
+	<classpathentry kind="src" path="prism-es2-eglfb/src"/>
+	<classpathentry kind="src" path="prism-es2-eglx11/src"/>
+	<classpathentry kind="src" path="prism-es2-ios/src"/>
+	<classpathentry kind="src" path="prism-es2-mac/src"/>
+	<classpathentry kind="src" path="prism-es2-win/src"/>
+	<classpathentry kind="src" path="prism-es2-x11/src"/>
+	<classpathentry kind="src" path="prism-es2/build/gensrc">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" path="prism-j2d/src"/>
+	<classpathentry kind="src" path="prism-sw/src"/>
+	<classpathentry kind="src" path="prism-null/src"/>
+	<classpathentry kind="src" path="prism-ps/src"/>
+	<classpathentry kind="src" path="prism-ps/build/gensrc"/>
+	<classpathentry kind="src" path="prism-ps/test"/>
+	<!--
+	<classpathentry kind="src" path="prism-ps/shadergen"/>
+	-->
+	<classpathentry kind="src" path="prism-util/src"/>
+
+	<classpathentry kind="src" path="test-stub-toolkit/src"/>
+	<classpathentry kind="src" path="tests/app-lifecycle/ClassLoaderApp/src"/>
+	<classpathentry kind="src" path="tests/app-lifecycle/ClassLoaderTest/test"/>
+	<classpathentry kind="src" path="tests/app-lifecycle/LauncherTests/test"/>
+	<classpathentry kind="src" path="tests/app-lifecycle/LifeCycleTests/test"/>
+	<classpathentry kind="src" path="tests/glass/ExceptionHandler/test"/>
+	<classpathentry kind="src" path="tests/graphics/MaterialTests/test"/>
+	<classpathentry kind="src" path="tests/graphics/SnapshotTests/test"/>
+	<classpathentry kind="src" path="tests/graphics/SwingInterop/test"/>
+	<classpathentry kind="src" path="tests/manual/printing"/>
+	<classpathentry kind="src" path="tests/quantum/CloseWindow/test"/>
+	<classpathentry kind="src" path="tests/quantum/WindowSceneInitDispose/test"/>
+
+	<!-- Use webview/src (instead of ../caches/sdk/build/lib/desktop/jfxwebview.jar) -->
+	<classpathentry kind="src" path="webview/src"/>
+	<!--
+	<classpathentry kind="src" path="webview/test"/>
+	-->
+	<!-- 
+	The "webview/native/WebKitBuild/Release/WebCore/generated/java" folder is only
+	available when webkit is built. Instead of forcing webkit to be build we 
+	include "../caches/sdk/build/lib/desktop/jfxwebkit.jar" as fallback.
+	-->
+	<classpathentry combineaccessrules="false" kind="src" path="webview/native/WebKitBuild/Release/WebCore/generated/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="lib" path="../caches/sdk/build/lib/desktop/jfxwebkit.jar">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+
+	<classpathentry kind="src" path="/rt-closed"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/media">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="lib" path="../caches/sdk/build/lib/desktop/jfxmedia.jar">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+
+	<classpathentry kind="src" path="/org.eclipse.swt">
+		<attributes>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="lib" path="../import/swt-3.7.1/swt-debug.jar"/>
+	<classpathentry kind="output" path="bin"/>
+
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+	<classpathentry kind="lib" path="../import/antlr-3.1.3/antlr-3.1.3/lib/antlr-3.1.3.jar"/>
+	<classpathentry kind="lib" path="../import/findbugs-2.0.1/findbugs-2.0.1/lib/ant.jar"/>
+</classpath>
--- a/.hgignore	Mon Jun 17 18:00:06 2013 -0700
+++ b/.hgignore	Wed Jun 19 13:57:16 2013 -0700
@@ -6,6 +6,8 @@
 \.conflict\~$
 ^\.ant-targets.*$
 \.idea/workspace.xml$
+^\.gradle/
+^buildSrc/\.gradle/
 build/
 bin/
 dist/
--- a/.hgtags	Mon Jun 17 18:00:06 2013 -0700
+++ b/.hgtags	Wed Jun 19 13:57:16 2013 -0700
@@ -79,3 +79,6 @@
 e23ed8f2acc15347ad2afa76e0b41e40fbc840c1 8.0-b89
 c0354b5fc20196e8579ee7d51e7e75f5d6dac244 8.0-b90
 f160d58b21eff89bef9d1558def1ebf72cad1ba0 8.0-b91
+018368ab732728b77c028a7cb1b2b5f179c683ff 8.0-b92
+40836a4b4d8551cea4da57ea54d62d4f9f6871c4 8.0-b93
+7203a2531ae96ae8cef4ac79e5d4cb7d8c73d75f 8.0-b94
--- a/apps/build.xml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/build.xml	Wed Jun 19 13:57:16 2013 -0700
@@ -11,10 +11,12 @@
 
     <target name="jar-apps" depends="init">
         <ant dir="samples" target="jar-apps" inheritAll="false"/>
+        <ant dir="experiments" target="jar-apps" inheritAll="false"/>
     </target>
 
     <target name="clean" depends="init">
         <ant dir="samples" target="clean" inheritAll="false"/>
+        <ant dir="experiments" target="clean" inheritAll="false"/>
     </target>
 
     <target name="jfx-apps" depends="jar-apps,dist-apps,copy-public-apps">
--- a/apps/experiments/3DViewer/3D Viewer.iml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/3D Viewer.iml	Wed Jun 19 13:57:16 2013 -0700
@@ -58,7 +58,9 @@
     <orderEntry type="module" module-name="prism-sw" />
     <orderEntry type="module" module-name="prism-util" />
     <orderEntry type="module" module-name="test-stub-toolkit" />
-    <orderEntry type="module" module-name="webkit" />
+    <orderEntry type="module" module-name="webview" />
+    <orderEntry type="module" module-name="javafx-builders" />
+    <orderEntry type="module" module-name="javafx-font-t2k" />
   </component>
 </module>
 
--- a/apps/experiments/3DViewer/build.xml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/build.xml	Wed Jun 19 13:57:16 2013 -0700
@@ -9,6 +9,7 @@
 <!-- in the project's Project Properties dialog box.-->
 <project name="3DViewer" default="default" basedir=".">
     <description>Builds, tests, and runs the project 3DViewer.</description>
+    <import file="../../build-tasks.xml"/>
     <import file="nbproject/build-impl.xml"/>
     <!--
 
@@ -51,8 +52,7 @@
       -init-macrodef-junit:     defines macro for junit execution
       -init-macrodef-debug:     defines macro for class debugging
       -init-macrodef-java:      defines macro for class execution
-      -do-jar-with-manifest:    JAR building (if you are using a manifest)
-      -do-jar-without-manifest: JAR building (if you are not using a manifest)
+      -do-jar:                  JAR building
       run:                      execution of project 
       -javadoc-build:           Javadoc generation
       test-report:              JUnit report generation
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/exporters/fxml/FXMLExporter.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/exporters/fxml/FXMLExporter.java	Wed Jun 19 13:57:16 2013 -0700
@@ -35,11 +35,9 @@
 import java.io.FileNotFoundException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
-import java.lang.reflect.Array;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -48,6 +46,7 @@
 import java.util.TreeSet;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import javafx.collections.ObservableArray;
 import javafx.geometry.Point3D;
 import javafx.scene.Node;
 import javafx.scene.Parent;
@@ -147,11 +146,10 @@
                 res.add(new Property(aClass.getMethod("getMesh"), "mesh"));
             }
             if (TriangleMesh.class.isAssignableFrom(aClass)) {
-                TriangleMesh tm;
-                res.add(new Property(aClass.getMethod("getPoints", float[].class), "points"));
-                res.add(new Property(aClass.getMethod("getTexCoords", float[].class), "texCoords"));
-                res.add(new Property(aClass.getMethod("getFaces", int[].class), "faces"));
-                res.add(new Property(aClass.getMethod("getFaceSmoothingGroups", int[].class), "faceSmoothingGroups"));
+                res.add(new Property(aClass.getMethod("getPoints"), "points"));
+                res.add(new Property(aClass.getMethod("getTexCoords"), "texCoords"));
+                res.add(new Property(aClass.getMethod("getFaces"), "faces"));
+                res.add(new Property(aClass.getMethod("getFaceSmoothingGroups"), "faceSmoothingGroups"));
             }
         } catch (NoSuchMethodException | SecurityException ex) {
             Logger.getLogger(FXMLExporter.class.getName()).log(Level.SEVERE, null, ex);
@@ -201,8 +199,8 @@
                                 container.addChild(exportToFXML(item));
                             }
                         }
-                    } else if (value instanceof int[] || value instanceof float[]) {
-                        int length = Array.getLength(value);
+                    } else if (value instanceof ObservableArray) {
+                        int length = ((ObservableArray) value).size();
                         if (length > 0) {
                             FXML container = fxml.addContainer(property.name);
                             container.setValue(value);
@@ -222,12 +220,6 @@
             }
         }
 
-        if (object instanceof Parent && ((Parent) object).getChildrenUnmodifiable().size() > 0) {
-            FXML container = fxml.addContainer("children");
-            for (Node child : ((Parent) object).getChildrenUnmodifiable()) {
-                container.addChild(exportToFXML(child));
-            }
-        }
         return fxml;
     }
 
@@ -315,12 +307,10 @@
                 }
                 if (value != null) {
                     String toString;
-                    if (value instanceof float[]) {
-                        toString = Arrays.toString((float[]) value);
-                    } else if (value instanceof int[]) {
-                        toString = Arrays.toString((int[]) value);
+                    if (value instanceof ObservableArray) {
+                        toString = value.toString();
                     } else {
-                        throw new UnsupportedOperationException("Only float[] and int[] arrays are currently supported");
+                        throw new UnsupportedOperationException("Only ObservableArrays are currently supported");
                     }
                     printWriter.append(indent1).append(toString.substring(1, toString.length() - 1)).append("\n");
                 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/exporters/javasource/JavaSourceExporter.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,489 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.javafx.experiments.exporters.javasource;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.value.WritableValue;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.MeshView;
+import javafx.scene.shape.TriangleMesh;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Scale;
+import javafx.scene.transform.Transform;
+import javafx.scene.transform.Translate;
+import javafx.util.Duration;
+import com.javafx.experiments.importers.Optimizer;
+import com.javafx.experiments.importers.maya.MayaImporter;
+import com.sun.javafx.animation.TickCalculation;
+import com.sun.scenario.animation.NumberTangentInterpolator;
+import com.sun.scenario.animation.SplineInterpolator;
+
+/**
+ * A exporter for 3D Models and animations that creates a Java Source file.
+ */
+public class JavaSourceExporter {
+    private int nodeCount = 0;
+    private int materialCount = 0;
+    private int meshCount = 0;
+    private int meshViewCount = 0;
+    private int methodCount = 1;
+    private int translateCount = 0;
+    private int rotateCount = 0;
+    private int scaleCount = 0;
+    private Map<WritableValue,String> writableVarMap = new HashMap<>();
+    private StringBuilder nodeCode = new StringBuilder();
+    private StringBuilder timelineCode = new StringBuilder();
+    private final boolean hasTimeline;
+    private final String baseUrl;
+    private final String className;
+    private final String packageName;
+    private final File outputFile;
+
+    public JavaSourceExporter(String baseUrl, Node rootNode, Timeline timeline, File outputFile) {
+        this(baseUrl, rootNode, timeline,computePackageName(baseUrl), outputFile);
+    }
+
+    public JavaSourceExporter(String baseUrl, Node rootNode, Timeline timeline, String packageName, File outputFile) {
+        this.baseUrl =
+                (baseUrl.charAt(baseUrl.length()-1) == '/') ?
+                        baseUrl.replaceAll("/+","/") :
+                        baseUrl.replaceAll("/+","/") + '/';
+        this.hasTimeline = timeline != null;
+        this.className = outputFile.getName().substring(0,outputFile.getName().lastIndexOf('.'));
+        this.packageName = packageName;
+        this.outputFile = outputFile;
+        process("        ",rootNode);
+        if (hasTimeline) process("        ",timeline);
+    }
+
+    private static String computePackageName(String baseUrl) {
+        // remove protocol from baseUrl
+        System.out.println("JavaSourceExporter.computePackageName   baseUrl = " + baseUrl);
+        baseUrl = baseUrl.replaceAll("^[a-z]+:/+","/");
+        System.out.println("baseUrl = " + baseUrl);
+        // try and work out package name from file
+        StringBuilder packageName = new StringBuilder();
+        String[] pathSegments = baseUrl.split(File.separatorChar == '\\'?"\\\\":"/");
+        System.out.println("pathSegments = " + Arrays.toString(pathSegments));
+        loop: for (int i = pathSegments.length-1; i >= 0; i -- ) {
+            switch (pathSegments[i]){
+                case "com":
+                case "org":
+                case "net":
+                    packageName.insert(0,pathSegments[i]);
+                    break loop;
+                case "src":
+                    packageName.deleteCharAt(0);
+                    break loop;
+                case "main":
+                    if ("src".equals(pathSegments[i-1])) {
+                        packageName.deleteCharAt(0);
+                        break loop;
+                    }
+                default:
+                    packageName.insert(0,'.'+pathSegments[i]);
+                    break;
+            }
+            // if we get all way to root of file system then we have failed
+            if (i==0) packageName = null;
+        }
+        System.out.println(" packageName = " + packageName);
+        return packageName==null?null:packageName.toString();
+    }
+
+    public void export() {
+        try {
+            BufferedWriter out = new BufferedWriter(new FileWriter(outputFile));
+//            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
+            StringBuilder nodeVars = new StringBuilder();
+            nodeVars.append("    private static Node ");
+            for (int i=0; i<nodeCount; i++) {
+                if (i!=0) nodeVars.append(',');
+                nodeVars.append("NODE_"+i);
+            }
+            nodeVars.append(";\n");
+            nodeVars.append("    private static TriangleMesh ");
+            for (int i=0; i<meshCount; i++) {
+                if (i!=0) nodeVars.append(',');
+                nodeVars.append("MESH_"+i);
+            }
+            nodeVars.append(";\n");
+            if (translateCount > 0) {
+                nodeVars.append("    private static Translate ");
+                for (int i=0; i<translateCount; i++) {
+                    if (i!=0) nodeVars.append(',');
+                    nodeVars.append("TRANS_"+i);
+                }
+                nodeVars.append(";\n");
+            }
+            if (rotateCount > 0) {
+                nodeVars.append("    private static Rotate ");
+                for (int i=0; i<rotateCount; i++) {
+                    if (i!=0) nodeVars.append(',');
+                    nodeVars.append("ROT_"+i);
+                }
+                nodeVars.append(";\n");
+            }
+            if (scaleCount > 0) {
+                nodeVars.append("    private static Scale ");
+                for (int i=0; i<scaleCount; i++) {
+                    if (i!=0) nodeVars.append(',');
+                    nodeVars.append("SCALE_"+i);
+                }
+                nodeVars.append(";\n");
+            }
+            nodeVars.append("    public static final MeshView[] MESHVIEWS = new MeshView[]{ ");
+            for (int i=0; i<meshViewCount; i++) {
+                if (i!=0) nodeVars.append(',');
+                nodeVars.append("new MeshView()");
+            }
+            nodeVars.append("};\n");
+//            nodeVars.append("    public static final MeshView ");
+//            for (int i=0; i<meshViewCount; i++) {
+//                if (i!=0) nodeVars.append(',');
+//                nodeVars.append("MESHVIEW_"+i+" = new MeshView()");
+//            }
+//            nodeVars.append(";\n");
+
+            nodeVars.append("    public static final Node ROOT;\n");
+            nodeVars.append("    public static final Map<String,MeshView> MESHVIEW_MAP;\n");
+            if (hasTimeline) {
+                nodeVars.append("    public static final Timeline TIMELINE = new Timeline();\n");
+            }
+            StringBuilder methodCode = new StringBuilder();
+            for (int m=0; m< methodCount;m++) {
+                methodCode.append("        method"+m+"();\n");
+            }
+
+            if (packageName != null) out.write(
+                    "package "+packageName+";\n\n");
+            out.write(
+                    "import java.util.*;\n" +
+                    "import javafx.util.Duration;\n" +
+                    "import javafx.animation.*;\n" +
+                    "import javafx.scene.*;\n" +
+                    "import javafx.scene.paint.*;\n" +
+                    "import javafx.scene.image.*;\n" +
+                    "import javafx.scene.shape.*;\n" +
+                    "import javafx.scene.transform.*;\n\n" +
+                    "public class "+className+" {\n" +
+                    nodeVars +
+                    "    // ======== NODE CODE ===============\n" +
+                    "    private static void method0(){\n" +
+                    nodeCode +
+                    "    }\n" +
+                    "    static {\n" +
+                    methodCode +
+                    "        // ======== TIMELINE CODE ===============\n" +
+                    timelineCode +
+                    "        // ======== SET PUBLIC VARS ===============\n" +
+                    "        Map<String,MeshView> meshViewMap = new HashMap<String,MeshView>();\n" +
+                    "        for (MeshView meshView: MESHVIEWS) meshViewMap.put(meshView.getId(),meshView);\n" +
+                    "        MESHVIEW_MAP = Collections.unmodifiableMap(meshViewMap);\n" +
+                    "        ROOT = NODE_0;\n" +
+                    "    }\n" +
+                    "}\n");
+
+            out.flush();
+            out.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private int process(String indent, Node node) {
+        if (node instanceof MeshView) {
+            return process(indent,(MeshView)node);
+        } else if (node instanceof Group) {
+            return process(indent,(Group)node);
+        } else {
+            throw new UnsupportedOperationException("Found unknown node type: "+node.getClass().getName());
+        }
+    }
+
+    private int process(String indent, MeshView node) {
+        final int index = nodeCount ++;
+        final String varName = "NODE_"+index;
+        final int meshViewIndex = meshViewCount ++;
+        final String meshViewVarName = "MESHVIEWS["+meshViewIndex+"]";
+//        nodeCode.append(indent+meshViewVarName+" = new MeshView();\n");
+        nodeCode.append(indent+varName+" = "+meshViewVarName+";\n");
+        if (node.getId() != null) nodeCode.append(indent+meshViewVarName+".setId(\""+node.getId()+"\");\n");
+        nodeCode.append(indent+meshViewVarName+".setCullFace(CullFace."+node.getCullFace().name()+");\n");
+        processNodeTransforms(indent,meshViewVarName,node);
+        process(indent,meshViewVarName,(PhongMaterial)node.getMaterial());
+        process(indent,meshViewVarName,(TriangleMesh)node.getMesh());
+        return index;
+    }
+
+    private void processNodeTransforms(String indent, String varName, Node node) {
+        if (node.getTranslateX() != 0) nodeCode.append(indent+varName+".setTranslateX("+node.getTranslateX()+");\n");
+        if (node.getTranslateY() != 0) nodeCode.append(indent+varName+".setTranslateY("+node.getTranslateY()+");\n");
+        if (node.getTranslateZ() != 0) nodeCode.append(indent+varName+".setTranslateZ("+node.getTranslateZ()+");\n");
+        if (node.getRotate() != 0) nodeCode.append(indent+varName+".setRotate("+node.getRotate()+");\n");
+        if (!node.getTransforms().isEmpty()) {
+            nodeCode.append(indent+varName+".getTransforms().addAll(\n");
+            for (int i=0; i< node.getTransforms().size(); i++) {
+                if (i!=0) nodeCode.append(",\n");
+                Transform transform = node.getTransforms().get(i);
+                if (transform instanceof Translate) {
+                    Translate t = (Translate)transform;
+                    nodeCode.append(indent+"    "+storeTransform(t)+" = new Translate("+t.getX()+","+t.getY()+","+t.getZ()+")");
+                } else if (transform instanceof Scale) {
+                    Scale s = (Scale)transform;
+                    nodeCode.append(indent+"    "+storeTransform(s)+" = new Scale("+s.getX()+","+s.getY()+","+s.getZ()+","+s.getPivotX()+","+s.getPivotY()+","+s.getPivotZ()+")");
+                } else if (transform instanceof Rotate) {
+                    Rotate r = (Rotate)transform;
+                    nodeCode.append(indent+"    "+storeTransform(r)+" = new Rotate("+r.getAngle()+","+r.getPivotX()+","+r.getPivotY()+","+r.getPivotZ()+",");
+                    if (r.getAxis() == Rotate.X_AXIS) {
+                        nodeCode.append("Rotate.X_AXIS");
+                    } else if (r.getAxis() == Rotate.Y_AXIS) {
+                        nodeCode.append("Rotate.Y_AXIS");
+                    } else if (r.getAxis() == Rotate.Z_AXIS) {
+                        nodeCode.append("Rotate.Z_AXIS");
+                    }
+                    nodeCode.append(")");
+                } else {
+                    throw new UnsupportedOperationException("Unknown Transform Type: "+transform.getClass());
+                }
+
+            }
+            nodeCode.append("\n"+indent+");\n");
+        }
+    }
+
+    private String storeTransform(Transform transform) {
+        String varName;
+        if (transform instanceof Translate) {
+            final int index = translateCount ++;
+            varName = "TRANS_"+index;
+            Translate t = (Translate)transform;
+            writableVarMap.put(t.xProperty(),varName+".xProperty()");
+            writableVarMap.put(t.yProperty(),varName+".yProperty()");
+            writableVarMap.put(t.zProperty(),varName+".zProperty()");
+        } else if (transform instanceof Scale) {
+            final int index = scaleCount ++;
+            varName = "SCALE_"+index;
+            Scale s = (Scale)transform;
+            writableVarMap.put(s.xProperty(),varName+".xProperty()");
+            writableVarMap.put(s.yProperty(),varName+".yProperty()");
+            writableVarMap.put(s.zProperty(),varName+".zProperty()");
+            writableVarMap.put(s.pivotXProperty(),varName+".pivotXProperty()");
+            writableVarMap.put(s.pivotYProperty(),varName+".pivotYProperty()");
+            writableVarMap.put(s.pivotZProperty(),varName+".pivotZProperty()");
+        } else if (transform instanceof Rotate) {
+            final int index = rotateCount ++;
+            varName = "ROT_"+index;
+            Rotate r = (Rotate)transform;
+            writableVarMap.put(r.angleProperty(),varName+".angleProperty()");
+        } else {
+            throw new UnsupportedOperationException("Unknown Transform Type: "+transform.getClass());
+        }
+        return varName;
+    }
+
+    private int process(String indent, Group node) {
+        final int index = nodeCount ++;
+        final String varName = "NODE_"+index;
+        List<Integer> childIndex = new ArrayList<>();
+        for (int i = 0; i < node.getChildren().size(); i++) {
+            Node child = node.getChildren().get(i);
+            childIndex.add(
+                    process(indent,child));
+        }
+        nodeCode.append(indent+varName+" = new Group(");
+        for (int i = 0; i < childIndex.size(); i++) {
+            if (i!=0) nodeCode.append(',');
+            nodeCode.append("NODE_"+childIndex.get(i));
+        }
+        nodeCode.append(");\n");
+        processNodeTransforms(indent, varName, node);
+        nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
+        return index;
+    }
+
+    private void process(String indent, String varName, TriangleMesh mesh) {
+        final int index = meshCount ++;
+        final String meshName = "MESH_"+index;
+
+        nodeCode.append(indent+meshName+" = new TriangleMesh();\n");
+        nodeCode.append(indent+varName+".setMesh("+meshName+");\n");
+
+        nodeCode.append(indent+meshName+".getPoints().ensureCapacity("+mesh.getPoints().size()+");\n");
+        nodeCode.append(indent + meshName + ".getPoints().addAll(");
+        for (int i = 0; i < mesh.getPoints().size(); i++) {
+            if (i!=0) nodeCode.append(',');
+            nodeCode.append(mesh.getPoints().get(i)+"f");
+        }
+        nodeCode.append(");\n");
+        nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
+        nodeCode.append(indent+meshName+".getTexCoords().ensureCapacity("+mesh.getTexCoords().size()+");\n");
+        nodeCode.append(indent + meshName + ".getTexCoords().addAll(");
+        for (int i = 0; i < mesh.getTexCoords().size(); i++) {
+            if (i!=0) nodeCode.append(',');
+            nodeCode.append(mesh.getTexCoords().get(i)+"f");
+        }
+        nodeCode.append(");\n");
+        nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
+        nodeCode.append(indent+meshName+".getFaces().ensureCapacity("+mesh.getFaces().size()+");\n");
+        nodeCode.append(indent + meshName + ".getFaces().addAll(");
+        for (int i = 0; i < mesh.getFaces().size(); i++) {
+            if ((i%5000) == 0 && i > 0) {
+                nodeCode.append(");\n");
+                nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
+                nodeCode.append(indent+meshName+".getFaces().addAll(");
+            } else if (i!=0) {
+                nodeCode.append(',');
+            }
+            nodeCode.append(mesh.getFaces().get(i));
+        }
+        nodeCode.append(");\n");
+        nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
+        nodeCode.append(indent+meshName+".getFaceSmoothingGroups().ensureCapacity("+mesh.getFaceSmoothingGroups().size()+");\n");
+        nodeCode.append(indent+meshName+".getFaceSmoothingGroups().addAll(");
+        for (int i = 0; i < mesh.getFaceSmoothingGroups().size(); i++) {
+            if (i!=0) nodeCode.append(',');
+            nodeCode.append(mesh.getFaceSmoothingGroups().get(i));
+        }
+        nodeCode.append(");\n");
+
+//        nodeCode.append(indent+varName+".setMesh("+process((TriangleMesh)node.getMesh())+");\n");
+    }
+
+    private void process(String indent, String varName, PhongMaterial material) {
+        final int index = materialCount ++;
+        final String materialName = "MATERIAL_"+index;
+
+        nodeCode.append(indent + "PhongMaterial " + materialName + " = new PhongMaterial();\n");
+        nodeCode.append(indent + materialName + ".setDiffuseColor(" + toCode(material.getDiffuseColor()) + ");\n");
+        String specColor = toCode(material.getSpecularColor());
+        if (specColor!=null) nodeCode.append(indent + materialName + ".setSpecularColor(" + specColor + ");\n");
+        nodeCode.append(indent + materialName + ".setSpecularPower("+material.getSpecularPower()+");\n");
+        if (material.getDiffuseMap() != null) {
+            nodeCode.append(indent + "try {\n");
+            nodeCode.append(indent + "    " + materialName + ".setDiffuseMap("+toString(material.getDiffuseMap())+");\n");
+            nodeCode.append(indent + "} catch (NullPointerException npe) {\n");
+            nodeCode.append(indent + "    System.err.println(\"Could not load texture resource ["+material.getDiffuseMap().impl_getUrl()+"]\");\n");
+            nodeCode.append(indent + "}\n");
+        }
+        if (material.getBumpMap() != null) {
+            nodeCode.append(indent + materialName + ".setBumpMap("+toString(material.getBumpMap())+");\n");
+        }
+        if (material.getSpecularMap() != null) {
+            nodeCode.append(indent + materialName + ".setSpecularMap()("+toString(material.getSpecularMap())+");\n");
+        }
+        if (material.getSelfIlluminationMap() != null) {
+            nodeCode.append(indent + materialName + ".setSelfIlluminationMap()("+toString(material.getSelfIlluminationMap())+");\n");
+        }
+        nodeCode.append(indent+varName+".setMaterial("+materialName+");\n");
+    }
+
+    private String toString(Image image) {
+        String url = image.impl_getUrl();
+        if (url.startsWith(baseUrl)) {
+            return  "new Image("+className+".class.getResource(\""+url.substring(baseUrl.length())+"\").toExternalForm())";
+        } else {
+            return "new Image(\""+url+"\")";
+        }
+    }
+
+    private void process(String indent, Timeline timeline) {
+        int count = 0;
+        for (KeyFrame keyFrame: timeline.getKeyFrames()) {
+            if (keyFrame.getValues().isEmpty()) continue;
+            nodeCode.append(indent+"TIMELINE.getKeyFrames().add(new KeyFrame(Duration.millis("+keyFrame.getTime().toMillis()+"d),\n");
+            boolean firstKeyValue = true;
+            for (KeyValue keyValue: keyFrame.getValues()) {
+                if (firstKeyValue) {
+                    firstKeyValue = false;
+                } else {
+                    nodeCode.append(",\n");
+                }
+                String var = writableVarMap.get(keyValue.getTarget());
+                if (var == null) System.err.println("Failed to find writable value in map for : "+keyValue.getTarget());
+                nodeCode.append(indent+"    new KeyValue("+var+","+keyValue.getEndValue()+","+toString(keyValue.getInterpolator())+")");
+            }
+            nodeCode.append("\n"+indent+"));\n");
+
+            if (count > 0 && ((count % 10) == 0)) {
+                nodeCode.append("    }\n    private static void method" + (methodCount++) + "(){\n");
+            }
+            count ++;
+        }
+    }
+
+    private String toString(Interpolator interpolator) {
+//        if (interpolator == Interpolator.DISCRETE || true) {
+        if (interpolator == Interpolator.DISCRETE) {
+            return "Interpolator.DISCRETE";
+        } else if (interpolator == Interpolator.EASE_BOTH) {
+            return "Interpolator.EASE_BOTH";
+        } else if (interpolator == Interpolator.EASE_IN) {
+            return "Interpolator.EASE_IN";
+        } else if (interpolator == Interpolator.EASE_OUT) {
+            return "Interpolator.EASE_OUT";
+        } else if (interpolator == Interpolator.LINEAR) {
+            return "Interpolator.LINEAR";
+        } else if (interpolator instanceof SplineInterpolator) {
+            SplineInterpolator si = (SplineInterpolator)interpolator;
+            return "Interpolator.SPLINE("+si.getX1()+"d,"+si.getY1()+"d,"+si.getX2()+"d,"+si.getY2()+"d)";
+        } else if (interpolator instanceof NumberTangentInterpolator) {
+            NumberTangentInterpolator ti = (NumberTangentInterpolator)interpolator;
+            return "Interpolator.TANGENT(Duration.millis("+ TickCalculation.toMillis((long)ti.getInTicks())+"d),"+ti.getInValue()
+                    +"d,Duration.millis("+TickCalculation.toMillis((long)ti.getInTicks())+"d),"+ti.getOutValue()+"d)";
+        } else {
+            throw new UnsupportedOperationException("Unknown Interpolator type: "+interpolator.getClass());
+        }
+    }
+
+    private String toCode(Color color) {
+        return color == null ? null : "new Color("+color.getRed()+","+color.getGreen()+","+color.getBlue()+","+color.getOpacity()+")";
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalApp.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.javafx.experiments.height2normal;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import javax.imageio.ImageIO;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.application.Application;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Group;
+import javafx.scene.PerspectiveCamera;
+import javafx.scene.Scene;
+import javafx.scene.SubScene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.Separator;
+import javafx.scene.control.Slider;
+import javafx.scene.control.ToolBar;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.Box;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Translate;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import com.javafx.experiments.jfx3dviewer.AutoScalingGroup;
+
+/**
+ * Gui Util App for converting Heights 2 Normals
+ */
+public class Height2NormalApp  extends Application {
+    private Image testImage;
+    private SimpleObjectProperty<Image> heightImage = new SimpleObjectProperty<>();
+    private SimpleObjectProperty<Image> normalImage = new SimpleObjectProperty<>();
+    private File heightFile;
+    private Stage stage;
+
+    @Override public void start(Stage stage) throws Exception {
+        this.stage = stage;
+
+        // load test image
+        testImage = new Image(Height2NormalApp.class.getResource("javafx-heightmap.jpg").toExternalForm());
+        heightImage.set(testImage);
+
+        // create toolbar
+        ToolBar toolBar = new ToolBar();
+        Button openButton = new Button("Open...");
+        openButton.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                open();
+            }
+        });
+        Button saveButton = new Button("Save...");
+        saveButton.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                save();
+            }
+        });
+        final CheckBox invertCheckBox = new CheckBox("invert");
+        final Slider scaleSlider = new Slider(1,50,2);
+        toolBar.getItems().addAll(openButton,saveButton,
+                                  new Separator(),
+                                  invertCheckBox,
+                                  new Separator(),
+                                  new Label("Scale:"),scaleSlider);
+
+        // bind creation of normal image
+        normalImage.bind(
+                new ObjectBinding<Image>() {
+                    { bind(heightImage, invertCheckBox.selectedProperty(), scaleSlider.valueProperty()); }
+                    @Override protected Image computeValue() {
+                        return Height2NormalConverter.convertToNormals(heightImage.get(), invertCheckBox.isSelected(), scaleSlider.getValue());
+                    }
+                });
+
+        // build stage
+        VBox root = new VBox();
+        HBox views = new HBox();
+        VBox.setVgrow(views, Priority.ALWAYS);
+        root.getChildren().addAll(toolBar, views);
+
+        ImageView srcImageView = new ImageView();
+        srcImageView.setFitWidth(512);
+        srcImageView.setFitHeight(512);
+        srcImageView.imageProperty().bind(heightImage);
+        ImageView dstImageView = new ImageView();
+        dstImageView.setFitWidth(512);
+        dstImageView.setFitHeight(512);
+        dstImageView.imageProperty().bind(normalImage);
+        views.getChildren().addAll(srcImageView,dstImageView,new View3D().create());
+
+        Scene scene = new Scene(root);
+        stage.setScene(scene);
+        stage.show();
+    }
+
+    public void open() {
+        FileChooser fileChooser = new FileChooser();
+        heightFile = fileChooser.showOpenDialog(stage);
+        if (heightFile != null) {
+            try {
+                heightImage.set(new Image(heightFile.toURI().toURL().toExternalForm()));
+            } catch (MalformedURLException e) {
+                e.printStackTrace();
+            }
+        } else {
+            heightImage.set(testImage);
+        }
+    }
+
+    public void save() {
+        FileChooser fileChooser = new FileChooser();
+        if (heightFile != null) {
+            String filePath = heightFile.getName();
+            fileChooser.setInitialFileName(filePath.substring(0,filePath.lastIndexOf('.'))+"-normal-map.png");
+        } else {
+            fileChooser.setInitialFileName("normal-map.png");
+        }
+        File normalFile = fileChooser.showSaveDialog(stage);
+        if (normalFile != null) {
+            try {
+                ImageIO.write(SwingFXUtils.fromFXImage(normalImage.get(),null),"png",normalFile);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    private class View3D {
+        private final Group root3D = new Group();
+        private final PerspectiveCamera camera = new PerspectiveCamera(true);
+        private final Rotate cameraXRotate = new Rotate(-40,0,0,0,Rotate.X_AXIS);
+        private final Rotate cameraYRotate = new Rotate(-20,0,0,0,Rotate.Y_AXIS);
+        private final Rotate cameraLookXRotate = new Rotate(0,0,0,0,Rotate.X_AXIS);
+        private final Rotate cameraLookZRotate = new Rotate(0,0,0,0,Rotate.Z_AXIS);
+        private final Translate cameraPosition = new Translate(0,0,-7);
+        private AutoScalingGroup autoScalingGroup = new AutoScalingGroup(2);
+
+        public SubScene create() {
+            SubScene scene = new SubScene(root3D,512,512,true,false);
+            scene.setFill(Color.ALICEBLUE);
+
+            // CAMERA
+            camera.getTransforms().addAll(
+                    cameraXRotate,
+                    cameraYRotate,
+                    cameraPosition,
+                    cameraLookXRotate,
+                    cameraLookZRotate);
+            camera.setNearClip(0.1);
+            camera.setFarClip(100);
+            scene.setCamera(camera);
+            root3D.getChildren().addAll(camera, autoScalingGroup);
+
+            Box box = new Box(10,0.11,10);
+
+            PhongMaterial material = new PhongMaterial(Color.DODGERBLUE);
+            material.bumpMapProperty().bind(normalImage);
+            box.setMaterial(material);
+
+            autoScalingGroup.getChildren().add(box);
+
+            Timeline timeline = new Timeline(
+                    new KeyFrame(Duration.ZERO, new KeyValue(cameraYRotate.angleProperty(),0)),
+                    new KeyFrame(Duration.seconds(10), new KeyValue(cameraYRotate.angleProperty(),360))
+            );
+            timeline.setCycleCount(Timeline.INDEFINITE);
+            timeline.play();
+
+            return scene;
+        }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalConverter.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.javafx.experiments.height2normal;
+
+import javafx.geometry.Point3D;
+import javafx.scene.image.Image;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.PixelReader;
+import javafx.scene.image.WritableImage;
+
+/**
+ * Util class to convert height maps into normal maps
+ */
+public class Height2NormalConverter {
+    public static Image convertToNormals(Image heightMap, boolean invert, double scale) {
+        final int w = (int)heightMap.getWidth();
+        final int h = (int)heightMap.getHeight();
+        final byte[] heightPixels = new byte[w*h*4];
+        final byte[] normalPixels = new byte[w*h*4];
+        // get pixels
+        final PixelReader reader = heightMap.getPixelReader();
+        reader.getPixels(0, 0, w, h, PixelFormat.getByteBgraInstance(),heightPixels,0,w*4);
+        if (invert) {
+            for (int y=0; y<h; y++) {
+                for (int x=0; x<w; x++) {
+                    final int pixelIndex = (y*w*4) + (x*4);
+                    heightPixels[pixelIndex] = (byte)(255-Byte.toUnsignedInt(heightPixels[pixelIndex]));
+                    heightPixels[pixelIndex+1] = (byte)(255-Byte.toUnsignedInt(heightPixels[pixelIndex+1]));
+                    heightPixels[pixelIndex+2] = (byte)(255-Byte.toUnsignedInt(heightPixels[pixelIndex+2]));
+                    heightPixels[pixelIndex+3] = heightPixels[pixelIndex+3];
+                }
+            }
+        }
+        // generate normal map
+        for (int y=0; y<h; y++) {
+            for (int x=0; x<w; x++) {
+                final int yAbove = Math.max(0,y-1);
+                final int yBelow = Math.min(h - 1, y + 1);
+                final int xLeft = Math.max(0, x - 1);
+                final int xRight = Math.min(w - 1, x + 1);
+                final int pixelIndex = (y*w*4) + (x*4);
+                final int pixelAboveIndex = (yAbove*w*4) + (x*4);
+                final int pixelBelowIndex = (yBelow*w*4) + (x*4);
+                final int pixelLeftIndex = (y*w*4) + (xLeft*4);
+                final int pixelRightIndex = (y*w*4) + (xRight*4);
+                final int pixelAboveHeight = Byte.toUnsignedInt(heightPixels[pixelAboveIndex]);
+                final int pixelBelowHeight = Byte.toUnsignedInt(heightPixels[pixelBelowIndex]);
+                final int pixelLeftHeight = Byte.toUnsignedInt(heightPixels[pixelLeftIndex]);
+                final int pixelRightHeight = Byte.toUnsignedInt(heightPixels[pixelRightIndex]);
+
+                Point3D pixelAbove = new Point3D(x,yAbove,pixelAboveHeight);
+                Point3D pixelBelow = new Point3D(x,yBelow,pixelBelowHeight);
+                Point3D pixelLeft = new Point3D(xLeft,y,pixelLeftHeight);
+                Point3D pixelRight = new Point3D(xRight,y,pixelRightHeight);
+                Point3D H = pixelLeft.subtract(pixelRight);
+                Point3D V = pixelAbove.subtract(pixelBelow);
+                Point3D normal = H.crossProduct(V);
+                // normalize normal
+                normal = new Point3D(
+                        normal.getX()/w,
+                        normal.getY()/h,
+                        normal.getZ()
+                );
+                // it seems there is lots of ways to calculate the Z element of normal map, 3 options here
+//                normalPixels[pixelIndex] = (byte)((normal.getZ()*128)+128); // Option 1
+                normalPixels[pixelIndex] = (byte)(255-(normal.getZ() * scale)); // Option 2
+//                normalPixels[pixelIndex] = (byte)255; // Option 3
+                normalPixels[pixelIndex+1] = (byte)((normal.getY()*128)+128);
+                normalPixels[pixelIndex+2] = (byte)((normal.getX()*128)+128);
+                normalPixels[pixelIndex+3] = (byte)255;
+            }
+        }
+        // create output image
+        final WritableImage outImage = new WritableImage(w,h);
+        outImage.getPixelWriter().setPixels(0,0,w,h,PixelFormat.getByteBgraInstance(),normalPixels,0,w*4);
+        return outImage;
+    }
+}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/Importer3D.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/Importer3D.java	Wed Jun 19 13:57:16 2013 -0700
@@ -8,6 +8,7 @@
 import javafx.scene.Node;
 import javafx.scene.shape.MeshView;
 import javafx.scene.shape.TriangleMesh;
+import javafx.util.Pair;
 import com.javafx.experiments.importers.dae.DaeImporter;
 import com.javafx.experiments.importers.max.MaxLoader;
 import com.javafx.experiments.importers.maya.MayaGroup;
@@ -30,13 +31,37 @@
     }
 
     /**
-     * Load a 3D file.
+     * Load a 3D file, always loaded as TriangleMesh.
      *
      * @param fileUrl The url of the 3D file to load
      * @return The loaded Node which could be a MeshView or a Group
      * @throws IOException if issue loading file
      */
     public static Node load(String fileUrl) throws IOException {
+        return load(fileUrl,false);
+    }
+
+    /**
+     * Load a 3D file.
+     *
+     * @param fileUrl The url of the 3D file to load
+     * @param asPolygonMesh When true load as a PolygonMesh if the loader supports
+     * @return The loaded Node which could be a MeshView or a Group
+     * @throws IOException if issue loading file
+     */
+    public static Node load(String fileUrl, boolean asPolygonMesh) throws IOException {
+        return loadIncludingAnimation(fileUrl,asPolygonMesh).getKey();
+    }
+
+    /**
+     * Load a 3D file.
+     *
+     * @param fileUrl The url of the 3D file to load
+     * @param asPolygonMesh When true load as a PolygonMesh if the loader supports
+     * @return The loaded Node which could be a MeshView or a Group and the Timeline animation
+     * @throws IOException if issue loading file
+     */
+    public static Pair<Node,Timeline> loadIncludingAnimation(String fileUrl, boolean asPolygonMesh) throws IOException {
         // get extension
         final int dot = fileUrl.lastIndexOf('.');
         if (dot <= 0) {
@@ -45,51 +70,41 @@
         final String extension = fileUrl.substring(dot + 1, fileUrl.length()).toLowerCase();
         switch (extension) {
             case "ma":
-                return loadMayaFile(fileUrl);
+                final MayaImporter mayaImporter = new MayaImporter();
+                mayaImporter.load(fileUrl, asPolygonMesh);
+                final Timeline timeline = mayaImporter.getTimeline();
+                return new Pair<Node, Timeline>(mayaImporter.getRoot(),timeline);
             case "ase":
-                return loadMaxFile(fileUrl);
+                return new Pair<Node, Timeline>(new MaxLoader().loadMaxUrl(fileUrl),null);
             case "obj":
-                return loadObjFile(fileUrl);
+                final Group res = new Group();
+                if (asPolygonMesh) {
+                    PolyObjImporter reader = new PolyObjImporter(fileUrl);
+                    for (String key : reader.getMeshes()) {
+                        res.getChildren().add(reader.buildPolygonMeshView(key));
+                    }
+                } else {
+                    ObjImporter reader = new ObjImporter(fileUrl);
+                    for (String key : reader.getMeshes()) {
+                        res.getChildren().add(reader.buildMeshView(key));
+                    }
+                }
+                return new Pair<Node, Timeline>(res,null);
             case "fxml":
-                Object fxmlRoot = FXMLLoader.load(new URL(fileUrl));
+                final Object fxmlRoot = FXMLLoader.load(new URL(fileUrl));
                 if (fxmlRoot instanceof Node) {
-                    return (Node)fxmlRoot;
+                    return new Pair<Node, Timeline>((Node)fxmlRoot,null);
                 } else if (fxmlRoot instanceof TriangleMesh) {
-                    return new MeshView((TriangleMesh)fxmlRoot);
+                    return new Pair<Node, Timeline>(new MeshView((TriangleMesh)fxmlRoot),null);
                 }
                 throw new IOException("Unknown object in FXML file ["+fxmlRoot.getClass().getName()+"]");
             case "dae":
-                return loadDaeFile(fileUrl);
+                final DaeImporter daeImporter = new DaeImporter(fileUrl, true);
+                return new Pair<Node, Timeline>(
+                        daeImporter.getRootNode(),
+                        null);
             default:
                 throw new IOException("Unknown 3D file format ["+extension+"]");
         }
     }
-
-    private static MayaGroup loadMayaFile(String fileUrl) throws IOException {
-        MayaImporter mayaImporter = new MayaImporter();
-        mayaImporter.load(fileUrl);
-        Timeline timeline = mayaImporter.getTimeline();
-        timeline.setCycleCount(Timeline.INDEFINITE);
-        timeline.play();
-        return mayaImporter.getRoot();
-    }
-
-    private static Node loadMaxFile(String fileUrl) throws IOException {
-        return new MaxLoader().loadMaxUrl(fileUrl);
-    }
-
-    private static Node loadObjFile(String fileUrl) throws IOException {
-//        ObjImporter reader = new ObjImporter(fileUrl);
-        PolyObjImporter reader = new PolyObjImporter(fileUrl);
-        Group res = new Group();
-        for (String key : reader.getMeshes()) {
-            res.getChildren().add(reader.buildPolygonMeshView(key));
-        }
-        return res;
-    }
-
-    private static Node loadDaeFile(String fileUrl) throws IOException {
-        DaeImporter importer = new DaeImporter(fileUrl, true);
-        return importer.getRootNode();
-    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/Optimizer.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,556 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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 General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.javafx.experiments.importers;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.property.Property;
+import javafx.beans.value.WritableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableFloatArray;
+import javafx.collections.ObservableIntegerArray;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.SortedList;
+import javafx.geometry.Point2D;
+import javafx.geometry.Point3D;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.shape.MeshView;
+import javafx.scene.shape.TriangleMesh;
+import static javafx.scene.shape.TriangleMesh.*;
+import javafx.scene.transform.Transform;
+import javafx.util.Duration;
+
+/**
+ * Optimizer to take 3D model and timeline loaded by one of the importers and do as much optimization on
+ * the scene graph that was create as we can while still being able to play the given animation.
+ */
+public class Optimizer {
+
+    private Timeline timeline;
+    private Node root;
+    private Set<Transform> bound = new HashSet<>();
+    private List<Parent> emptyParents = new ArrayList<>();
+    private List<MeshView> meshViews = new ArrayList<>();
+    private boolean convertToDiscrete = true;
+
+    public Optimizer(Timeline timeline, Node root) {
+        this(timeline, root, false);
+    }
+
+    public Optimizer(Timeline timeline, Node root, boolean convertToDiscrete) {
+        this.timeline = timeline;
+        this.root = root;
+        this.convertToDiscrete = convertToDiscrete;
+    }
+
+    private int trRemoved, trTotal, groupsTotal, trCandidate, trEmpty;
+
+    public void optimize() {
+        trRemoved = 0;
+        trTotal = 0;
+        trCandidate = 0;
+        trEmpty = 0;
+        groupsTotal = 0;
+        emptyParents.clear();
+
+        parseTimeline();
+        optimize(root);
+        removeEmptyGroups();
+        optimizeMeshes();
+
+        System.out.printf("removed %d (%.2f%%) out of total %d transforms\n", trRemoved, 100d * trRemoved / trTotal, trTotal);
+        System.out.printf("there are %d more multiplications that can be done of matrices that never change\n", trCandidate);
+        System.out.printf("there are %d (%.2f%%) out of total %d groups with no transforms in them\n", trEmpty, 100d * trEmpty / groupsTotal, groupsTotal);
+    }
+
+    private void optimize(Node node) {
+        ObservableList<Transform> transforms = node.getTransforms();
+        Iterator<Transform> iterator = transforms.iterator();
+        boolean prevIsStatic = false;
+        while (iterator.hasNext()) {
+            Transform transform = iterator.next();
+            trTotal++;
+            if (transform.isIdentity()) {
+                if (timeline == null || !bound.contains(transform)) {
+                    iterator.remove();
+                    trRemoved++;
+                }
+            } else {
+                if (timeline == null || !bound.contains(transform)) {
+                    if (prevIsStatic) {
+                        trCandidate++;
+                    }
+                    prevIsStatic = true;
+                } else {
+                    prevIsStatic = false;
+                }
+            }
+        }
+        if (node instanceof Parent) {
+            groupsTotal++;
+            Parent p = (Parent) node;
+            for (Node n : p.getChildrenUnmodifiable()) {
+                optimize(n);
+            }
+            if (transforms.isEmpty()) {
+                Parent parent = p.getParent();
+                if (parent instanceof Group) {
+                    trEmpty++;
+//                    System.out.println("Empty group = " + node.getId());
+                    emptyParents.add(p);
+                } else {
+//                    System.err.println("parent is not group = " + parent);
+                }
+            }
+        }
+        if (node instanceof MeshView) {
+            meshViews.add((MeshView) node);
+        }
+    }
+
+    private void optimizeMeshes() {
+        optimizePoints();
+        optimizeTexCoords();
+        optimizeFaces();
+    }
+
+    private void optimizeFaces() {
+        int total = 0, sameIndexes = 0, samePoints = 0, smallArea = 0;
+        ObservableIntegerArray newFaces = FXCollections.observableIntegerArray();
+        ObservableIntegerArray newFaceSmoothingGroups = FXCollections.observableIntegerArray();
+        for (MeshView meshView : meshViews) {
+            TriangleMesh mesh = (TriangleMesh) meshView.getMesh();
+            ObservableIntegerArray faces = mesh.getFaces();
+            ObservableIntegerArray faceSmoothingGroups = mesh.getFaceSmoothingGroups();
+            ObservableFloatArray points = mesh.getPoints();
+            newFaces.clear();
+            newFaces.ensureCapacity(faces.size());
+            newFaceSmoothingGroups.clear();
+            newFaceSmoothingGroups.ensureCapacity(faceSmoothingGroups.size());
+            for (int i = 0; i < faces.size(); i += NUM_COMPONENTS_PER_FACE) {
+                total++;
+                int i1 = faces.get(i) * NUM_COMPONENTS_PER_POINT;
+                int i2 = faces.get(i + 2) * NUM_COMPONENTS_PER_POINT;
+                int i3 = faces.get(i + 4) * NUM_COMPONENTS_PER_POINT;
+                if (i1 == i2 || i1 == i3 || i2 == i3) {
+                    sameIndexes++;
+                    continue;
+                }
+                Point3D p1 = new Point3D(points.get(i1), points.get(i1 + 1), points.get(i1 + 2));
+                Point3D p2 = new Point3D(points.get(i2), points.get(i2 + 1), points.get(i2 + 2));
+                Point3D p3 = new Point3D(points.get(i3), points.get(i3 + 1), points.get(i3 + 2));
+                if (p1.equals(p2) || p1.equals(p3) || p2.equals(p3)) {
+                    samePoints++;
+                    continue;
+                }
+                double a = p1.distance(p2);
+                double b = p2.distance(p3);
+                double c = p3.distance(p1);
+                double p = (a + b + c) / 2;
+                double sqarea = p * (p - a) * (p - b) * (p - c);
+
+                final float DEAD_FACE = 1.f/1024/1024/1024/1024; // taken from MeshNormal code
+
+                if (sqarea < DEAD_FACE) {
+                    smallArea++;
+//                    System.out.printf("a = %e, b = %e, c = %e, sqarea = %e\n"
+//                            + "p1 = %s\np2 = %s\np3 = %s\n", a, b, c, sqarea, p1.toString(), p2.toString(), p3.toString());
+                    continue;
+                }
+                newFaces.addAll(faces, i, NUM_COMPONENTS_PER_FACE);
+                int fIndex = i / NUM_COMPONENTS_PER_FACE;
+                if (fIndex < faceSmoothingGroups.size()) {
+                    newFaceSmoothingGroups.addAll(faceSmoothingGroups.get(fIndex));
+                }
+            }
+            faces.setAll(newFaces);
+            faceSmoothingGroups.setAll(newFaceSmoothingGroups);
+            faces.trimToSize();
+            faceSmoothingGroups.trimToSize();
+        }
+        int badTotal = sameIndexes + samePoints + smallArea;
+        System.out.printf("Removed %d (%.2f%%) faces with same point indexes, "
+                + "%d (%.2f%%) faces with same points, "
+                + "%d (%.2f%%) faces with small area. "
+                + "Total %d (%.2f%%) bad faces out of %d total.\n",
+                sameIndexes, 100d * sameIndexes / total,
+                samePoints, 100d * samePoints / total,
+                smallArea, 100d * smallArea / total,
+                badTotal, 100d * badTotal / total, total);
+    }
+
+    private void optimizePoints() {
+        int total = 0, duplicates = 0, check = 0;
+
+        Map<Point3D, Integer> pp = new HashMap<>();
+        ObservableIntegerArray reindex = FXCollections.observableIntegerArray();
+        ObservableFloatArray newPoints = FXCollections.observableFloatArray();
+
+        for (MeshView meshView : meshViews) {
+            TriangleMesh mesh = (TriangleMesh) meshView.getMesh();
+            ObservableFloatArray points = mesh.getPoints();
+            int os = points.size() / NUM_COMPONENTS_PER_POINT;
+
+            pp.clear();
+            newPoints.clear();
+            newPoints.ensureCapacity(points.size());
+            reindex.clear();
+            reindex.resize(os);
+
+            for (int i = 0, oi = 0, ni = 0; i < points.size(); i += NUM_COMPONENTS_PER_POINT, oi++) {
+                float x = points.get(i);
+                float y = points.get(i + 1);
+                float z = points.get(i + 2);
+                Point3D p = new Point3D(x, y, z);
+                Integer index = pp.get(p);
+                if (index == null) {
+                    pp.put(p, ni);
+                    reindex.set(oi, ni);
+                    newPoints.addAll(x, y, z);
+                    ni++;
+                } else {
+                    reindex.set(oi, index);
+                }
+            }
+
+            int ns = newPoints.size() / NUM_COMPONENTS_PER_POINT;
+
+            int d = os - ns;
+            duplicates += d;
+            total += os;
+
+            points.setAll(newPoints);
+            points.trimToSize();
+
+            ObservableIntegerArray faces = mesh.getFaces();
+            for (int i = 0; i < faces.size(); i += 2) {
+                faces.set(i, reindex.get(faces.get(i)));
+            }
+
+//            System.out.printf("There are %d (%.2f%%) duplicate points out of %d total for mesh '%s'.\n",
+//                    d, 100d * d / os, os, meshView.getId());
+
+            check += mesh.getPoints().size() / NUM_COMPONENTS_PER_POINT;
+        }
+        System.out.printf("There are %d (%.2f%%) duplicate points out of %d total.\n",
+                duplicates, 100d * duplicates / total, total);
+        System.out.printf("Now we have %d points.\n", check);
+    }
+
+    private void optimizeTexCoords() {
+        int total = 0, duplicates = 0, check = 0;
+
+        Map<Point2D, Integer> pp = new HashMap<>();
+        ObservableIntegerArray reindex = FXCollections.observableIntegerArray();
+        ObservableFloatArray newTexCoords = FXCollections.observableFloatArray();
+
+        for (MeshView meshView : meshViews) {
+            TriangleMesh mesh = (TriangleMesh) meshView.getMesh();
+            ObservableFloatArray texcoords = mesh.getTexCoords();
+            int os = texcoords.size() / NUM_COMPONENTS_PER_TEXCOORD;
+
+            pp.clear();
+            newTexCoords.clear();
+            newTexCoords.ensureCapacity(texcoords.size());
+            reindex.clear();
+            reindex.resize(os);
+
+            for (int i = 0, oi = 0, ni = 0; i < texcoords.size(); i += NUM_COMPONENTS_PER_TEXCOORD, oi++) {
+                float x = texcoords.get(i);
+                float y = texcoords.get(i + 1);
+                Point2D p = new Point2D(x, y);
+                Integer index = pp.get(p);
+                if (index == null) {
+                    pp.put(p, ni);
+                    reindex.set(oi, ni);
+                    newTexCoords.addAll(x, y);
+                    ni++;
+                } else {
+                    reindex.set(oi, index);
+                }
+            }
+
+            int ns = newTexCoords.size() / NUM_COMPONENTS_PER_TEXCOORD;
+
+            int d = os - ns;
+            duplicates += d;
+            total += os;
+
+            texcoords.setAll(newTexCoords);
+            texcoords.trimToSize();
+
+            ObservableIntegerArray faces = mesh.getFaces();
+            for (int i = 1; i < faces.size(); i += 2) {
+                faces.set(i, reindex.get(faces.get(i)));
+            }
+
+//            System.out.printf("There are %d (%.2f%%) duplicate texcoords out of %d total for mesh '%s'.\n",
+//                    d, 100d * d / os, os, meshView.getId());
+
+            check += mesh.getTexCoords().size() / NUM_COMPONENTS_PER_TEXCOORD;
+        }
+        System.out.printf("There are %d (%.2f%%) duplicate texcoords out of %d total.\n",
+                duplicates, 100d * duplicates / total, total);
+        System.out.printf("Now we have %d texcoords.\n", check);
+    }
+
+    private void cleanUpRepeatingFramesAndValues() {
+        ObservableList<KeyFrame> timelineKeyFrames = timeline.getKeyFrames().sorted(new KeyFrameComparator());
+//        Timeline timeline;
+        int kfTotal = timelineKeyFrames.size(), kfRemoved = 0;
+        int kvTotal = 0, kvRemoved = 0;
+        Map<Duration, KeyFrame> kfUnique = new HashMap<>();
+        Map<WritableValue, KeyValue> kvUnique = new HashMap<>();
+        MapOfLists<KeyFrame, KeyFrame> duplicates = new MapOfLists<>();
+        Iterator<KeyFrame> iterator = timelineKeyFrames.iterator();
+        while (iterator.hasNext()) {
+            KeyFrame duplicate = iterator.next();
+            KeyFrame original = kfUnique.put(duplicate.getTime(), duplicate);
+            if (original != null) {
+                kfRemoved++;
+                iterator.remove(); // removing duplicate keyFrame
+                duplicates.add(original, duplicate);
+
+                kfUnique.put(duplicate.getTime(), original);
+            }
+            kvUnique.clear();
+            for (KeyValue kvDup : duplicate.getValues()) {
+                kvTotal++;
+                KeyValue kvOrig = kvUnique.put(kvDup.getTarget(), kvDup);
+                if (kvOrig != null) {
+                    kvRemoved++;
+                    if (!kvOrig.getEndValue().equals(kvDup.getEndValue()) && kvOrig.getTarget() == kvDup.getTarget()) {
+                        System.err.println("KeyValues set different values for KeyFrame " + duplicate.getTime() + ":"
+                                + "\n kvOrig = " + kvOrig + ", \nkvDup = " + kvDup);
+                    }
+                }
+            }
+        }
+        for (KeyFrame orig : duplicates.keySet()) {
+            List<KeyValue> keyValues = new ArrayList<>();
+            for (KeyFrame dup : duplicates.get(orig)) {
+                keyValues.addAll(dup.getValues());
+            }
+            timelineKeyFrames.set(timelineKeyFrames.indexOf(orig),
+                    new KeyFrame(orig.getTime(), keyValues.toArray(new KeyValue[keyValues.size()])));
+        }
+        System.out.printf("Removed %d (%.2f%%) duplicate KeyFrames out of total %d.\n",
+                kfRemoved, 100d * kfRemoved / kfTotal, kfTotal);
+        System.out.printf("Identified %d (%.2f%%) duplicate KeyValues out of total %d.\n",
+                kvRemoved, 100d * kvRemoved / kvTotal, kvTotal);
+    }
+
+    private static class KeyInfo {
+        KeyFrame keyFrame;
+        KeyValue keyValue;
+        boolean first;
+
+        public KeyInfo(KeyFrame keyFrame, KeyValue keyValue) {
+            this.keyFrame = keyFrame;
+            this.keyValue = keyValue;
+            first = false;
+        }
+
+        public KeyInfo(KeyFrame keyFrame, KeyValue keyValue, boolean first) {
+            this.keyFrame = keyFrame;
+            this.keyValue = keyValue;
+            this.first = first;
+        }
+    }
+
+    private static class MapOfLists<K, V> extends HashMap<K, List<V>> {
+
+        public void add(K key, V value) {
+            List<V> p = get(key);
+            if (p == null) {
+                p = new ArrayList<>();
+                put(key, p);
+            }
+            p.add(value);
+        }
+    }
+    
+    private void parseTimeline() {
+        bound.clear();
+        if (timeline == null) {
+            return;
+        }
+//        cleanUpRepeatingFramesAndValues(); // we don't need it usually as timeline is initially correct
+        SortedList<KeyFrame> sortedKeyFrames = timeline.getKeyFrames().sorted(new KeyFrameComparator());
+        MapOfLists<KeyFrame, KeyValue> toRemove = new MapOfLists<>();
+        Map<WritableValue, KeyInfo> prevValues = new HashMap<>();
+        Map<WritableValue, KeyInfo> prevPrevValues = new HashMap<>();
+        int kvTotal = 0;
+        for (KeyFrame keyFrame : sortedKeyFrames) {
+            for (KeyValue keyValue : keyFrame.getValues()) {
+                WritableValue<?> target = keyValue.getTarget();
+                KeyInfo prev = prevValues.get(target);
+                kvTotal++;
+                if (prev != null && prev.keyValue.getEndValue().equals(keyValue.getEndValue())) {
+//                if (prev != null && (prev.keyValue.equals(keyValue) || (prev.first && prev.keyValue.getEndValue().equals(keyValue.getEndValue())))) {
+                    KeyInfo prevPrev = prevPrevValues.get(target);
+                    if ((prevPrev != null && prevPrev.keyValue.getEndValue().equals(keyValue.getEndValue()))
+                            || (prev.first && target.getValue().equals(prev.keyValue.getEndValue()))) {
+                        // All prevPrev, prev and current match, so prev can be removed
+                        // or prev is first and its value equals to the property existing value, so prev can be removed
+                        toRemove.add(prev.keyFrame, prev.keyValue);
+                    } else {
+                        prevPrevValues.put(target, prev);
+//                        KeyInfo oldKeyInfo = prevPrevValues.put(target, prev);
+//                        if (oldKeyInfo != null && oldKeyInfo.keyFrame.getTime().equals(prev.keyFrame.getTime())) {
+//                            System.err.println("prevPrev replaced more than once per keyFrame on " + target + "\n"
+//                                    + "old = " + oldKeyInfo.keyFrame.getTime() + ", " + oldKeyInfo.keyValue + "\n"
+//                                    + "new = " + prev.keyFrame.getTime() + ", " + prev.keyValue
+//                                    );
+//                        }
+                    }
+                }
+                KeyInfo oldPrev = prevValues.put(target, new KeyInfo(keyFrame, keyValue, prev == null));
+                if (oldPrev != null) prevPrevValues.put(target, oldPrev);
+            }
+        }
+        // Deal with ending keyValues
+        for (WritableValue target : prevValues.keySet()) {
+            KeyInfo prev = prevValues.get(target);
+            KeyInfo prevPrev = prevPrevValues.get(target);
+            if (prevPrev != null && prevPrev.keyValue.getEndValue().equals(prev.keyValue.getEndValue())) {
+                // prevPrev and prev match, so prev can be removed
+                toRemove.add(prev.keyFrame, prev.keyValue);
+            }
+        }
+        int kvRemoved = 0;
+        int kfRemoved = 0, kfTotal = timeline.getKeyFrames().size(), kfSimplified = 0, kfNotRemoved = 0;
+        // Removing unnecessary KeyValues and KeyFrames
+        List<KeyValue> newKeyValues = new ArrayList<>();
+        for (int i = 0; i < timeline.getKeyFrames().size(); i++) {
+            KeyFrame keyFrame = timeline.getKeyFrames().get(i);
+            List<KeyValue> keyValuesToRemove = toRemove.get(keyFrame);
+            if (keyValuesToRemove != null) {
+                newKeyValues.clear();
+                for (KeyValue keyValue : keyFrame.getValues()) {
+                    if (keyValuesToRemove.remove(keyValue)) {
+                        kvRemoved++;
+                    } else {
+                        if (convertToDiscrete) {
+                            newKeyValues.add(new KeyValue((WritableValue)keyValue.getTarget(), keyValue.getEndValue(), Interpolator.DISCRETE));
+                        } else {
+                            newKeyValues.add(keyValue);
+                        }
+                    }
+                }
+            } else if (convertToDiscrete) {
+                newKeyValues.clear();
+                for (KeyValue keyValue : keyFrame.getValues()) {
+                    newKeyValues.add(new KeyValue((WritableValue)keyValue.getTarget(), keyValue.getEndValue(), Interpolator.DISCRETE));
+                }
+            }
+            if (keyValuesToRemove != null || convertToDiscrete) {
+                if (newKeyValues.isEmpty()) {
+                    if (keyFrame.getOnFinished() == null) {
+                        if (keyFrame.getName() != null) {
+                            System.err.println("Removed KeyFrame with name = " + keyFrame.getName());
+                        }
+                        timeline.getKeyFrames().remove(i);
+                        i--;
+                        kfRemoved++;
+                        continue; // for i
+                    } else {
+                        kfNotRemoved++;
+                    }
+                } else {
+                    keyFrame = new KeyFrame(keyFrame.getTime(), keyFrame.getName(), keyFrame.getOnFinished(), newKeyValues);
+                    timeline.getKeyFrames().set(i, keyFrame);
+                    kfSimplified++;
+                }
+            }
+            // collecting bound targets
+            for (KeyValue keyValue : keyFrame.getValues()) {
+                WritableValue<?> target = keyValue.getTarget();
+                if (target instanceof Property) {
+                    Property p = (Property) target;
+                    Object bean = p.getBean();
+                    if (bean instanceof Transform) {
+                        bound.add((Transform) bean);
+                    } else {
+                        throw new UnsupportedOperationException("Bean is not transform, bean = " + bean);
+                    }
+                } else {
+                    throw new UnsupportedOperationException("WritableValue is not property, can't identify what it changes, target = " + target);
+                }
+            }
+        }
+//        System.out.println("bound.size() = " + bound.size());
+        System.out.printf("Removed %d (%.2f%%) repeating KeyValues out of total %d.\n", kvRemoved, 100d * kvRemoved / kvTotal, kvTotal);
+        System.out.printf("Removed %d (%.2f%%) and simplified %d (%.2f%%) KeyFrames out of total %d. %d (%.2f%%) were not removed due to event handler attached.\n",
+                kfRemoved, 100d * kfRemoved / kfTotal,
+                kfSimplified, 100d * kfSimplified / kfTotal, kfTotal, kfNotRemoved, 100d * kfNotRemoved / kfTotal);
+        int check = 0;
+        for (KeyFrame keyFrame : timeline.getKeyFrames()) {
+            check += keyFrame.getValues().size();
+//            for (KeyValue keyValue : keyFrame.getValues()) {
+//                if (keyValue.getInterpolator() != Interpolator.DISCRETE) {
+//                    throw new IllegalStateException();
+//                }
+//            }
+        }
+        System.out.printf("Now there are %d KeyValues and %d KeyFrames.\n", check, timeline.getKeyFrames().size());
+    }
+
+    private void removeEmptyGroups() {
+        for (Parent p : emptyParents) {
+            Parent parent = p.getParent();
+            Group g = (Group) parent;
+            g.getChildren().addAll(p.getChildrenUnmodifiable());
+            g.getChildren().remove(p);
+        }
+    }
+
+    private static class KeyFrameComparator implements Comparator<KeyFrame> {
+
+        public KeyFrameComparator() {
+        }
+
+        @Override public int compare(KeyFrame o1, KeyFrame o2) {
+//            int compareTo = o1.getTime().compareTo(o2.getTime());
+//            if (compareTo == 0 && o1 != o2) {
+//                System.err.println("those two KeyFrames are equal: o1 = " + o1.getTime() + " and o2 = " + o2.getTime());
+//            }
+            return o1.getTime().compareTo(o2.getTime());
+        }
+    }
+
+}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/dae/DaeImporter.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/dae/DaeImporter.java	Wed Jun 19 13:57:16 2013 -0700
@@ -1,3 +1,34 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
 package com.javafx.experiments.importers.dae;
 
 import javafx.geometry.Point3D;
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/Loader.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/Loader.java	Wed Jun 19 13:57:16 2013 -0700
@@ -22,6 +22,7 @@
 import javafx.scene.image.Image;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.CullFace;
 import javafx.scene.shape.Mesh;
 import javafx.scene.shape.MeshView;
 import javafx.scene.shape.TriangleMesh;
@@ -42,7 +43,15 @@
 import com.javafx.experiments.importers.maya.values.MIntArray;
 import com.javafx.experiments.importers.maya.values.MPolyFace;
 import com.javafx.experiments.importers.maya.values.MString;
+import com.javafx.experiments.shape3d.PolygonMesh;
+import com.javafx.experiments.shape3d.PolygonMeshView;
+import com.javafx.experiments.shape3d.SkinningMesh;
 import com.sun.javafx.geom.Vec3f;
+import javafx.animation.AnimationTimer;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Scene;
+import javafx.scene.transform.Transform;
 
 /** Loader */
 class Loader {
@@ -91,14 +100,16 @@
     private int uvChannel;
     private MFloat3Array mPointTweaks;
     private URL url;
+    private boolean asPolygonMesh;
 
     //=========================================================================
     // Loader.load
     //-------------------------------------------------------------------------
     // Called from MayaImporter.load
     //=========================================================================
-    public void load(URL url) {
+    public void load(URL url, boolean asPolygonMesh) {
         this.url = url;
+        this.asPolygonMesh = asPolygonMesh;
         env = new MEnv();
         MParser parser = new MParser(env);
         try {
@@ -187,7 +198,7 @@
             } else if (n.isInstanceOf(fileType)) {
                 //                System.out.println("==> Found a node of fileType: " + n);
             } else if (n.isInstanceOf(skinClusterType)) {
-                // processClusterType(n);
+                processClusterType(n);
             } else if (n.isInstanceOf(meshType)) {
                 processMeshType(n, parentNode);
             } else if (n.isInstanceOf(jointType)) {
@@ -203,28 +214,20 @@
     protected void processClusterType(MNode n) {
         loaded.put(n, null);
         MArray ma = (MArray) n.getAttr("ma");
-        // NO_JOINTS
-        //        List<Joint> jointNodes = new ArrayList<Joint>();
-        //        for (int i = 0; i < ma.getSize(); i++) {
-        //            // hack...
-        //            MNode c = n.getIncomingConnectionToType("ma[" + i + "]", "joint");
-        //            Joint jn = (Joint) resolveNode(c);
-        //            jointNodes.add(jn);
-        ////                   //println("JOINT= {c.getName()} => {jn.id}");
-        //        }
 
+        List<Joint> jointNodes = new ArrayList<Joint>();
+        for (int i = 0; i < ma.getSize(); i++) {
+            // hack... ?
+            MNode c = n.getIncomingConnectionToType("ma[" + i + "]", "joint");
+            Joint jn = (Joint) resolveNode(c);
+            jointNodes.add(jn);
+        }
+        
         MNode outputMeshMNode = resolveOutputMesh(n);
         MNode inputMeshMNode = resolveInputMesh(n);
         if (inputMeshMNode == null || outputMeshMNode == null) {
             return;
         }
-        //               println("ORIG MESH FOR SKIN={origMesh} TARGET={targetMesh}");
-        //               MayaMeshNodeGroup targetMayaMeshNode = (MayaMeshNodeGroup) resolveNode(targetMesh);
-        MeshView targetMayaMeshNode = (MeshView) resolveNode(outputMeshMNode);
-        boolean nn = loaded.containsKey(outputMeshMNode);
-        //               println("TARGET MESH {targetMesh} {targetMesh.getNodeType().getName()} {targetMayaMeshNode} n={nn}");
-        //               var sourceMayaMeshNode = (resolveNode(origMesh) as MayaMeshNodeGroup);
-        MeshView sourceMayaMeshNode = (MeshView) resolveNode(inputMeshMNode);
         // We must be able to find the original converter in the meshConverters map
         MNode origOrigMesh = resolveOrigInputMesh(n);
         //               println("ORIG ORIG={origOrigMesh}");
@@ -233,94 +236,69 @@
         resolveNode(origOrigMesh).setVisible(false);
 
         MArray bindPreMatrixArray = (MArray) n.getAttr("pm");
-        //println("adding SKIN CONTROLLER to {targetMayaMeshNode}");
 
         Affine[] bindPreMatrix = new Affine[bindPreMatrixArray.getSize()];
         for (int i = 0; i < bindPreMatrixArray.getSize(); i++) {
             bindPreMatrix[i] = convertMatrix((MFloatArray) bindPreMatrixArray.getData(i));
         }
 
-        TriangleMesh sourceMesh = (TriangleMesh) sourceMayaMeshNode.getMesh();
-        TriangleMesh targetMesh = (TriangleMesh) targetMayaMeshNode.getMesh();
+        MArray mayaWeights = (MArray) n.getAttr("wl");
+        float[][] weights = new float [mayaWeights.getSize()][jointNodes.size()];
+        for (int i=0; i<mayaWeights.getSize(); i++) {
+            MFloatArray curWeights = (MFloatArray) mayaWeights.getData(i).getData("w");
+            for (int j = 0; j < jointNodes.size(); j++) {
+                weights[i][j] = j < curWeights.getSize() ? curWeights.get(j) : 0;
+            }
+        }
+        
+        Node sourceMayaMeshNode = resolveNode(inputMeshMNode);
+        Node targetMayaMeshNode = resolveNode(outputMeshMNode);
+            
+        if (sourceMayaMeshNode.getClass().equals(PolygonMeshView.class)) {
+            PolygonMeshView sourceMayaMeshView = (PolygonMeshView) sourceMayaMeshNode;
+            PolygonMeshView targetMayaMeshView = (PolygonMeshView) targetMayaMeshNode;
+            
+            PolygonMesh sourceMesh = (PolygonMesh) sourceMayaMeshView.getMesh();
+            Transform meshTransform = targetMayaMeshView.getLocalToSceneTransform();
+            SkinningMesh targetMesh = new SkinningMesh(sourceMesh, meshTransform, weights, bindPreMatrix, jointNodes);
+            targetMayaMeshView.setMesh(targetMesh);
 
-        MArray mayaWeights = (MArray) n.getAttr("wl");
-        //NO_JOINTS
-        //        int numJoints = jointNodes.size();
-        //
-        //        Map<Vertex, Vertex> source2target = new HashMap<Vertex, Vertex>(sourceMesh.getPointCount());
-        //
-        //        for (int vert = 0; vert < sourceMesh.getPointCount(); vert++) {
-        //
-        //            MFloatArray curWeights = (MFloatArray) mayaWeights.getData(vert).getData("w");
-        //            // NO_JOINTS
-        //            List<Double> influences = new ArrayList<Double>();
-        //           for (int j = 0; j < numJoints; j++) {
-        //               double w = j < curWeights.getSize() ? curWeights.get(j) : 0;
-        //               if (w != 0) {
-        //                   influences.add((double) j);
-        //                   influences.add(w);
-        //               }
-        //           }
-        //           int c = 0;
-        //           if (influences.size() > 0) {
-        //               //println("influences for {vert} = {for (k in influences) " {k} "}");
-        //               c = influences.size() / 2;
-        //           }
-        //
-        //            final int count = c;
-        //
-        //            Vertex v = sourceMesh.getVertices().get(vert);
-        //
-        //            if (DEBUG) {
-        //                System.out.println("vert=" + vert + " count=" + count);
-        //            }
-        //            // We create count of joints but only 4 of them will be used
-        //            int joints[] = new int[4];
-        //            float jointWeights[] = new float[4];
-        //            int jointIndex = 0;
-        //            for (int j = 0; j < count; j++) {
-        //                final int joint = influences.get(2 * j).intValue();
-        //                final double weight = influences.get(2 * j + 1);
-        //
-        //                if (sourceMesh.getJoints().size() == 0) {
-        //                    for (int k = 0; k < jointNodes.size(); k++) {
-        //                        Joint jointNode = jointNodes.get(k);
-        //                        sourceMesh.getJoints().add(new TriangleMesh.Joint(jointNode.getId(),
-        //                                bindPreMatrix[k], jointNode.getTransform()));
-        //                        jointNode.setInvertedInitialWorldTransform(bindPreMatrix[k]);
-        //                    }
-        //                }
-        //                if (Math.abs(weight) >= 1e-5 && jointIndex < 4) {
-        //                    joints[jointIndex] = joint;
-        //                    jointWeights[jointIndex] = (float) weight;
-        //                    jointIndex++;
-        //                }
-        //
-        //                if (DEBUG) {
-        //                    System.out.println("vert=" + vert + " v=" + v + " joint=" + joint + " weight=" + weight);
-        //                }
-        //            }
-        //            Vertex vr = new Vertex(v.x, v.y, v.z,
-        //                        joints[0], jointWeights[0],
-        //                        joints[1], jointWeights[1],
-        //                        joints[2], jointWeights[2],
-        //                        joints[3]);
-        //            source2target.put(v, vr);
-        //        }
-        //
-        //        for (Vertex v : sourceMesh.getVertices()) {
-        //            Vertex tv = source2target.get(v);
-        //            if (tv == null) {
-        //                tv = v;
-        //                source2target.put(v, tv);
-        //            }
-        //            targetMesh.getVertices().add(tv);
-        //        }
-        //        targetMesh.getJoints().addAll(sourceMesh.getJoints());// NO_JOINTS
-        targetMesh.getPoints().setAll(sourceMesh.getPoints());
-        targetMesh.getTexCoords().setAll(sourceMesh.getTexCoords());
-        targetMesh.getFaces().setAll(sourceMesh.getFaces());
-        targetMesh.getFaceSmoothingGroups().setAll(sourceMesh.getFaceSmoothingGroups());
+            final SkinningMeshTimer skinningMeshTimer = new SkinningMeshTimer(targetMesh);
+            if (targetMayaMeshNode.getScene() != null) {
+                skinningMeshTimer.start();
+            }
+            targetMayaMeshView.sceneProperty().addListener(new ChangeListener<Scene>() {
+                @Override
+                public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
+                    if (newValue == null) {
+                        skinningMeshTimer.stop();
+                    } else {
+                        skinningMeshTimer.start();
+                    }
+                }
+            });
+        } else {
+            Logger.getLogger(MayaImporter.class.getName()).log(Level.INFO, "Mesh skinning is not supported for triangle meshes. Select the 'Load as Polygons' option to load the mesh as polygon mesh.");
+            MeshView sourceMayaMeshView = (MeshView) sourceMayaMeshNode;
+            MeshView targetMayaMeshView = (MeshView) targetMayaMeshNode;
+            TriangleMesh sourceMesh = (TriangleMesh) sourceMayaMeshView.getMesh();
+            TriangleMesh targetMesh = (TriangleMesh) targetMayaMeshView.getMesh();
+            targetMesh.getPoints().setAll(sourceMesh.getPoints());
+            targetMesh.getTexCoords().setAll(sourceMesh.getTexCoords());
+            targetMesh.getFaces().setAll(sourceMesh.getFaces());
+            targetMesh.getFaceSmoothingGroups().setAll(sourceMesh.getFaceSmoothingGroups());
+        }
+    }
+    
+    private class SkinningMeshTimer extends AnimationTimer {
+        private SkinningMesh mesh;
+        SkinningMeshTimer(SkinningMesh mesh) {
+            this.mesh = mesh;
+        }
+        @Override
+        public void handle(long l) {
+            mesh.update();
+        }
     }
 
     protected Image loadImageFromFtnAttr(MNode fileNode, String name) {
@@ -475,21 +453,37 @@
             }
         }
 
-        Mesh mesh = convertToFXMesh(n);
+        Object mesh = convertToFXMesh(n);
 
-        MeshView mv = new MeshView();
-        mv.setId(n.getName());
-        mv.setMaterial(material);
-        //            mv.setWireframe(true);
-        //            mv.setAmbient(Color.GRAY); // TODO???
-        mv.setMesh(mesh);
+        if (asPolygonMesh) {
+            PolygonMeshView mv = new PolygonMeshView();
+            mv.setId(n.getName());
+            mv.setMaterial(material);
+            mv.setMesh((PolygonMesh) mesh);
+//            mv.setCullFace(CullFace.NONE); //TODO
+            loaded.put(n, mv);
+            if (node != null) {
+                ((Group) node).getChildren().add(mv);
+            }
+        } else {
+//            if (((TriangleMesh)mesh).getPoints().size() > 0) {
+                MeshView mv = new MeshView();
+                mv.setId(n.getName());
+                mv.setMaterial(material);
 
-        loaded.put(n, mv);
-        if (node != null) {
-            ((Group) node).getChildren().add(mv);
+    //            // TODO HACK for [JIRA] (RT-30449) FX 8 3D: Need to handle mirror transformation (flip culling);
+    //            mv.setCullFace(CullFace.FRONT);
+
+                mv.setMesh((TriangleMesh) mesh);
+
+                loaded.put(n, mv);
+                if (node != null) {
+                    ((Group) node).getChildren().add(mv);
+                }
+//            }
         }
     }
-
+            
     protected void processJointType(MNode n, Group parentNode) {
         // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
         Node result;
@@ -742,7 +736,7 @@
         }
     }
 
-    private Mesh convertToFXMesh(MNode n) {
+    private Object convertToFXMesh(MNode n) {
         mVerts = (MFloat3Array) n.getAttr("vt");
         MPolyFace mPolys = (MPolyFace) n.getAttr("fc");
         mPointTweaks = (MFloat3Array) n.getAttr("pt");
@@ -757,7 +751,11 @@
         }
 
         if (mPolys.getFaces() == null) {
-            return new TriangleMesh();
+            if (asPolygonMesh) {
+                return new PolygonMesh();
+            } else {
+                return new TriangleMesh();
+            }
         }
 
         MFloat3Array normals = (MFloat3Array) n.getAttr("n");
@@ -794,25 +792,24 @@
         return edgeVert(edgeNumber, false);
     }
 
-    private boolean addUVs(TriangleMesh mesh, int uvChannel) {
+    private float[] getTexCoords(int uvChannel) {
         if (uvSet == null || uvChannel < 0 || uvChannel >= uvSet.size()) {
-            return false;
+            return new float[] {0,0};
         }
         MCompound compound = (MCompound) uvSet.get(uvChannel);
         MFloat2Array uvs = (MFloat2Array) compound.getFieldData("uvsp");
         if (uvs == null || uvs.get() == null) {
-            return false;
+            return new float[] {0,0};
         }
 
-        float[] res = new float[uvs.getSize() * 2];
+        float[] texCoords = new float[uvs.getSize() * 2];
         float[] uvsData = uvs.get();
         for (int i = 0; i < uvs.getSize(); i++) {
             //note the 1 - v
-            res[i * 2] = uvsData[2 * i];
-            res[i * 2 + 1] = 1 - uvsData[2 * i + 1];
+            texCoords[i * 2] = uvsData[2 * i];
+            texCoords[i * 2 + 1] = 1 - uvsData[2 * i + 1];
         }
-        mesh.getTexCoords().setAll(res);
-        return true;
+        return texCoords;
     }
 
     private void getVert(int index, Vec3f vert) {
@@ -1195,7 +1192,7 @@
 
             float t = kt - EPSILON;
             if (t < 0.0) {
-                t = 0.0f;
+                continue; // just skipping all the negative frames
             }
 
             /*
@@ -1590,17 +1587,14 @@
         }
     }
 
-    private Mesh buildMeshData(List<MPolyFace.FaceData> faces, MFloat3Array normals) {
-
-        TriangleMesh mesh = new TriangleMesh();
-
+    private Object buildMeshData(List<MPolyFace.FaceData> faces, MFloat3Array normals) {
         // Setup vertexes
         float[] verts = mVerts.get();
         float[] tweaks = null;
         if (mPointTweaks != null) {
             tweaks = mPointTweaks.get();
         }
-        float[] points = new float[verts.length * 3];
+        float[] points = new float[verts.length];
         for (int index = 0; index < verts.length; index += 3) {
             if (tweaks != null && tweaks.length > index + 2) {
                 points[index] = verts[index] + tweaks[index];
@@ -1612,65 +1606,84 @@
                 points[index + 2] = verts[index + 2];
             }
         }
-        mesh.getPoints().setAll(points);
 
-        // copy UV as-is
-        if (!addUVs(mesh, uvChannel)) {
-            mesh.getTexCoords().setAll(0, 0);
-        }
-
-        int startFace = 0;
-        int endFace = faces.size();
+        // copy UV as-is (if any)
+        float[] texCoords = getTexCoords(uvChannel);
 
         int[] smGroups = SmoothGroups.calcSmoothGroups(faces, normals);
 
-        List<Integer> ff = new ArrayList<Integer>();
-        List<Integer> sg = new ArrayList<Integer>();
-
-        for (int f = startFace; f < endFace; f++) {
-            MPolyFace.FaceData faceData = faces.get(f);
-            int[] faceEdges = faceData.getFaceEdges();
-            int[][] uvData = faceData.getUVData();
-            int[] uvIndices = uvData == null ? null : uvData[uvChannel];
-            if (faceEdges != null && faceEdges.length > 0) {
-
-                // Generate triangle fan about the first vertex
-                int vIndex0 = edgeStart(faceEdges[0]);
-                int uvIndex0 = uvIndices == null ? 0 : uvIndices[0];
-
-                int vIndex1 = edgeStart(faceEdges[1]);
-                int uvIndex1 = uvIndices == null ? 0 : uvIndices[1];
-
-                for (int i = 2; i < faceEdges.length; i++) {
-                    int vIndex2 = edgeStart(faceEdges[i]);
-                    int uvIndex2 = uvIndices == null ? 0 : uvIndices[i];
-
-                    ff.add(vIndex0);
-                    ff.add(uvIndex0);
-                    ff.add(vIndex1);
-                    ff.add(uvIndex1);
-                    ff.add(vIndex2);
-                    ff.add(uvIndex2);
-                    sg.add(smGroups[f]);
-
-                    vIndex1 = vIndex2;
-                    uvIndex1 = uvIndex2;
+        if (asPolygonMesh) {
+            List<int[]> ff = new ArrayList<int[]>();
+            for (int f = 0; f < faces.size(); f++) {
+                MPolyFace.FaceData faceData = faces.get(f);
+                int[] faceEdges = faceData.getFaceEdges();
+                int[][] uvData = faceData.getUVData();
+                int[] uvIndices = uvData == null ? null : uvData[uvChannel];
+                if (faceEdges != null && faceEdges.length > 0) {
+                    int[] polyFace = new int[faceEdges.length * 2];
+                    for (int i = 0; i < faceEdges.length; i++) {
+                        int vIndex = edgeStart(faceEdges[i]);
+                        int uvIndex = uvIndices == null ? 0 : uvIndices[i];
+                        polyFace[i*2] = vIndex;
+                        polyFace[i*2+1] = uvIndex;
+                    }
+                    ff.add(polyFace);
                 }
             }
+            PolygonMesh mesh = new PolygonMesh(points, texCoords, ff.toArray(new int[ff.size()][]));
+            return mesh;
+        } else {
+            // Split the polygonal faces into triangle faces
+            List<Integer> ff = new ArrayList<Integer>();
+            List<Integer> sg = new ArrayList<Integer>();
+
+            for (int f = 0; f < faces.size(); f++) {
+                MPolyFace.FaceData faceData = faces.get(f);
+                int[] faceEdges = faceData.getFaceEdges();
+                int[][] uvData = faceData.getUVData();
+                int[] uvIndices = uvData == null ? null : uvData[uvChannel];
+                if (faceEdges != null && faceEdges.length > 0) {
+
+                    // Generate triangle fan about the first vertex
+                    int vIndex0 = edgeStart(faceEdges[0]);
+                    int uvIndex0 = uvIndices == null ? 0 : uvIndices[0];
+
+                    int vIndex1 = edgeStart(faceEdges[1]);
+                    int uvIndex1 = uvIndices == null ? 0 : uvIndices[1];
+
+                    for (int i = 2; i < faceEdges.length; i++) {
+                        int vIndex2 = edgeStart(faceEdges[i]);
+                        int uvIndex2 = uvIndices == null ? 0 : uvIndices[i];
+
+                        ff.add(vIndex0);
+                        ff.add(uvIndex0);
+                        ff.add(vIndex1);
+                        ff.add(uvIndex1);
+                        ff.add(vIndex2);
+                        ff.add(uvIndex2);
+                        sg.add(smGroups[f]);
+
+                        vIndex1 = vIndex2;
+                        uvIndex1 = uvIndex2;
+                    }
+                }
+            }
+            int[] fff = new int[ff.size()];
+            for (int i = 0; i < fff.length; i++) {
+                fff[i] = ff.get(i);
+            }
+            int[] sgsg = new int[sg.size()];
+            for (int i = 0; i < sgsg.length; i++) {
+                sgsg[i] = sg.get(i);
+            }
+
+            TriangleMesh mesh = new TriangleMesh();
+            mesh.getPoints().setAll(points);
+            mesh.getTexCoords().setAll(texCoords);
+            mesh.getFaces().setAll(fff);
+            mesh.getFaceSmoothingGroups().setAll(sgsg);
+            return mesh;
         }
-        int[] fff = new int[ff.size()];
-        for (int i = 0; i < fff.length; i++) {
-            fff[i] = ff.get(i);
-        }
-        mesh.getFaces().setAll(fff);
-        fff = null;
-        ff = null;
-        int[] sgsg = new int[sg.size()];
-        for (int i = 0; i < sgsg.length; i++) {
-            sgsg[i] = sg.get(i);
-        }
-        mesh.getFaceSmoothingGroups().setAll(sgsg);
-        return mesh;
     }
 
     MNode resolveOutputMesh(MNode n) {
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/MayaImporter.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/maya/MayaImporter.java	Wed Jun 19 13:57:16 2013 -0700
@@ -89,10 +89,10 @@
     //=========================================================================
     // MayaImporter.load
     //=========================================================================
-    public void load(String url) {
+    public void load(String url, boolean asPolygonMesh) {
         try {
             Loader loader = new Loader();
-            loader.load(new java.net.URL(url));
+            loader.load(new java.net.URL(url), asPolygonMesh);
 
             // This root is not automatically added to the scene.
             // It needs to be added by the user of MayaImporter.
@@ -133,12 +133,6 @@
                         (
                                 new KeyFrame(
                                         javafx.util.Duration.millis(e.getKey() * 1000f),
-                                        new EventHandler<ActionEvent>() {
-                                            @Override
-                                            public void handle(ActionEvent ev) {
-                                                // if (DEBUG) System.out.println("finished key frame at: " + e.getKey());
-                                            }
-                                        },
                                         (KeyValue[]) e.getValue().toArray(new KeyValue[e.getValue().size()])));
                 count++;
             }
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/obj/PolyObjImporter.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/importers/obj/PolyObjImporter.java	Wed Jun 19 13:57:16 2013 -0700
@@ -284,7 +284,7 @@
                 faceArrays
         );
         if (debug) {
-            System.out.println("mesh.points = " + Arrays.toString(mesh.points));
+            System.out.println("mesh.points = " + mesh.getPoints());
             System.out.println("mesh.texCoords = " + Arrays.toString(mesh.texCoords));
             System.out.println("mesh.faces: ");
             for (int[] face: mesh.faces) {
@@ -300,7 +300,7 @@
         meshes.put(key, mesh);
         materials.put(key, material);
         
-        log("Added mesh '" + key + "' of " + (mesh.points.length/3) + " vertexes, "
+        log("Added mesh '" + key + "' of " + (mesh.getPoints().size()/3) + " vertexes, "
                 + (mesh.texCoords.length/2) + " uvs, "
                 + mesh.faces.length + " faces, "
                 + 0 + " smoothing groups.");
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/ContentModel.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/ContentModel.java	Wed Jun 19 13:57:16 2013 -0700
@@ -31,9 +31,9 @@
  */
 package com.javafx.experiments.jfx3dviewer;
 
-import java.io.File;
-import java.io.IOException;
+import javafx.animation.Timeline;
 import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.event.EventHandler;
@@ -54,8 +54,8 @@
 import javafx.scene.shape.Sphere;
 import javafx.scene.transform.Rotate;
 import javafx.scene.transform.Translate;
-import com.javafx.experiments.importers.Importer3D;
 import com.javafx.experiments.shape3d.PolygonMeshView;
+import com.javafx.experiments.shape3d.SubDivision;
 
 /**
  * 3D Content Model for Viewer App. Contains the 3D scene and everything related to it: light, cameras etc.
@@ -77,6 +77,10 @@
     private PointLight light1 = new PointLight(Color.WHITE);
     private PointLight light2 = new PointLight(Color.ANTIQUEWHITE);
     private PointLight light3 = new PointLight(Color.ALICEBLUE);
+    private final SimpleObjectProperty<Timeline> timeline = new SimpleObjectProperty<>();
+    public Timeline getTimeline() { return timeline.get(); }
+    public SimpleObjectProperty<Timeline> timelineProperty() { return timeline; }
+    public void setTimeline(Timeline timeline) { this.timeline.set(timeline); }
     private SimpleBooleanProperty ambientLightEnabled = new SimpleBooleanProperty(false){
         @Override protected void invalidated() {
             if (get()) {
@@ -134,9 +138,12 @@
         }
     };
     private boolean wireframe = false;
-    private int subdivision = 0;
+    private int subdivisionLevel = 0;
+    private SubDivision.BoundaryMode boundaryMode = SubDivision.BoundaryMode.CREASE_EDGES;
+    private SubDivision.MapBorderMode mapBorderMode = SubDivision.MapBorderMode.NOT_SMOOTH;
+    private String loadedUrl = null;
 
-    public ContentModel(String fileToLoad) {
+    public ContentModel() {
         subScene = new SubScene(root3D,400,400,true,false);
         subScene.setFill(Color.ALICEBLUE);
 
@@ -157,28 +164,9 @@
                 System.out.println("z = " + newValue);
             }
         });
-        //LIGHTS
-//        root3D.getChildren().addAll(light1, light2, light3);
-        // BOX
-//        Box testBox = new Box(5,5,5);
-//        testBox.setMaterial(new PhongMaterial(Color.RED));
-//        testBox.setDrawMode(DrawMode.LINE);
-//        root3D.getChildren().add(testBox);
 
         root3D.getChildren().add(autoScalingGroup);
 
-        // LOAD DROP HERE MODEL
-        try {
-            if (fileToLoad != null) {
-                content = Importer3D.load(new File(fileToLoad).toURI().toURL().toExternalForm());
-            } else {
-                content = Importer3D.load(ContentModel.class.getResource("drop-here.obj").toExternalForm());
-            }
-            autoScalingGroup.getChildren().add(content);
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-
         // SCENE EVENT HANDLING FOR CAMERA NAV
         subScene.addEventHandler(MouseEvent.ANY, new EventHandler<MouseEvent>() {
             @Override public void handle(MouseEvent event) {
@@ -192,8 +180,8 @@
                     double yDelta = event.getSceneY() -  dragStartY;
                     cameraXRotate.setAngle(dragStartRotateX - (yDelta*0.7));
                     cameraYRotate.setAngle(dragStartRotateY + (xDelta*0.7));
+                    }
                 }
-            }
         });
         subScene.addEventHandler(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
             @Override public void handle(ScrollEvent event) {
@@ -203,6 +191,19 @@
                 cameraPosition.setZ(z);
             }
         });
+
+        SessionManager sessionManager = SessionManager.getSessionManager();
+        sessionManager.bind(cameraLookXRotate.angleProperty(), "cameraLookXRotate");
+        sessionManager.bind(cameraLookZRotate.angleProperty(), "cameraLookZRotate");
+        sessionManager.bind(cameraPosition.xProperty(), "cameraPosition.x");
+        sessionManager.bind(cameraPosition.yProperty(), "cameraPosition.y");
+        sessionManager.bind(cameraPosition.zProperty(), "cameraPosition.z");
+        sessionManager.bind(cameraXRotate.angleProperty(), "cameraXRotate");
+        sessionManager.bind(cameraYRotate.angleProperty(), "cameraYRotate");
+    }
+
+    public String getLoadedUrl() {
+        return loadedUrl;
     }
 
     public boolean getAmbientLightEnabled() {
@@ -306,7 +307,10 @@
         this.content = content;
         autoScalingGroup.getChildren().add(this.content);
         setWireFrame(content,wireframe);
-        setSubdivision(content,subdivision);
+        // TODO mesh is updated each time these are called even if no rendering needs to happen
+        setSubdivisionLevel(content, subdivisionLevel);
+        setBoundaryMode(content, boundaryMode);
+        setMapBorderMode(content, mapBorderMode);
     }
 
     public SubScene getSubScene() {
@@ -360,20 +364,48 @@
         }
     }
 
-    public int getSubdivision() {
-        return subdivision;
+    public SubDivision.BoundaryMode getBoundaryMode() {
+        return boundaryMode;
+    }
+    public void setBoundaryMode(SubDivision.BoundaryMode boundaryMode) {
+        this.boundaryMode = boundaryMode;
+        setBoundaryMode(root3D, boundaryMode);
+    }
+    private void setBoundaryMode(Node node, SubDivision.BoundaryMode boundaryMode) {
+        if (node instanceof PolygonMeshView) {
+            ((PolygonMeshView)node).setBoundaryMode(boundaryMode);
+        } else if (node instanceof Parent) {
+            for (Node child: ((Parent)node).getChildrenUnmodifiable()) setBoundaryMode(child, boundaryMode);
+        }
+    }
+    
+    public SubDivision.MapBorderMode getMapBorderMode() {
+        return mapBorderMode;
+    }
+    public void setMapBorderMode(SubDivision.MapBorderMode mapBorderMode) {
+        this.mapBorderMode = mapBorderMode;
+        setMapBorderMode(root3D, mapBorderMode);
+    }
+    private void setMapBorderMode(Node node, SubDivision.MapBorderMode mapBorderMode) {
+        if (node instanceof PolygonMeshView) {
+            ((PolygonMeshView)node).setMapBorderMode(mapBorderMode);
+        } else if (node instanceof Parent) {
+            for (Node child: ((Parent)node).getChildrenUnmodifiable()) setMapBorderMode(child, mapBorderMode);
+        }
     }
 
-    public void setSubdivision(int subdivision) {
-        this.subdivision = subdivision;
-        setSubdivision(root3D,subdivision);
+    public int getSubdivisionLevel() {
+        return subdivisionLevel;
     }
-
-    private void setSubdivision(Node node, int subdivision) {
+    public void setSubdivisionLevel(int subdivisionLevel) {
+        this.subdivisionLevel = subdivisionLevel;
+        setSubdivisionLevel(root3D, subdivisionLevel);
+    }
+    private void setSubdivisionLevel(Node node, int subdivisionLevel) {
         if (node instanceof PolygonMeshView) {
-            ((PolygonMeshView)node).setSubdivision(subdivision);
+            ((PolygonMeshView)node).setSubdivisionLevel(subdivisionLevel);
         } else if (node instanceof Parent) {
-            for (Node child: ((Parent)node).getChildrenUnmodifiable()) setSubdivision(child,subdivision);
+            for (Node child: ((Parent)node).getChildrenUnmodifiable()) setSubdivisionLevel(child, subdivisionLevel);
         }
     }
 
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/Jfx3dViewerApp.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/Jfx3dViewerApp.java	Wed Jun 19 13:57:16 2013 -0700
@@ -31,31 +31,52 @@
  */
 package com.javafx.experiments.jfx3dviewer;
 
+import java.io.File;
 import java.util.List;
 import javafx.application.Application;
+import javafx.event.EventHandler;
 import javafx.fxml.FXMLLoader;
 import javafx.scene.Parent;
 import javafx.scene.Scene;
 import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 
 /**
  * JavaFX 3D Viewer Application
  */
 public class Jfx3dViewerApp extends Application {
+    public static final String FILE_URL_PROPERTY = "fileUrl";
     private static ContentModel contentModel;
+    private SessionManager sessionManager;
 
     public static ContentModel getContentModel() {
         return contentModel;
     }
 
     @Override public void start(Stage stage) throws Exception {
+        sessionManager = SessionManager.createSessionManager("Jfx3dViewerApp");
+        sessionManager.loadSession();
+
         List<String> args = getParameters().getRaw();
-        contentModel = new ContentModel(args.isEmpty() ? null : args.get(0));
+        if (!args.isEmpty()) {
+            sessionManager.getProperties().setProperty(FILE_URL_PROPERTY, 
+                    new File(args.get(0)).toURI().toURL().toString());
+        }
+        contentModel = new ContentModel();
         Scene scene = new Scene(
                 FXMLLoader.<Parent>load(Jfx3dViewerApp.class.getResource("main.fxml")),
                 1024,600);
         stage.setScene(scene);
         stage.show();
+
+        stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
+            @Override
+            public void handle(WindowEvent event) {
+                sessionManager.saveSession();
+            }
+        });
+
+//        org.scenicview.ScenicView.show(contentModel.getSubScene().getRoot());
     }
 
     public static void main(String[] args) {
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/MainController.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/MainController.java	Wed Jun 19 13:57:16 2013 -0700
@@ -33,6 +33,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.util.ResourceBundle;
@@ -49,7 +50,9 @@
 import javafx.scene.Parent;
 import javafx.scene.control.Accordion;
 import javafx.scene.control.Button;
+import javafx.scene.control.CheckMenuItem;
 import javafx.scene.control.Label;
+import javafx.scene.control.SplitMenuButton;
 import javafx.scene.control.SplitPane;
 import javafx.scene.control.ToggleButton;
 import javafx.scene.input.DragEvent;
@@ -61,17 +64,33 @@
 import javafx.scene.shape.TriangleMesh;
 import javafx.stage.FileChooser;
 import javafx.util.Duration;
+import javafx.util.Pair;
 import com.javafx.experiments.exporters.fxml.FXMLExporter;
+import com.javafx.experiments.exporters.javasource.JavaSourceExporter;
 import com.javafx.experiments.importers.Importer3D;
+import com.javafx.experiments.importers.Optimizer;
+import java.net.URISyntaxException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javafx.geometry.BoundingBox;
 
 /**
  * Controller class for main fxml file.
  */
 public class MainController implements Initializable {
-    public Button openBtn;
+    public SplitMenuButton openMenuBtn;
     public Label status;
     public SplitPane splitPane;
     public ToggleButton settingsBtn;
+    public CheckMenuItem loadAsPolygonsCheckBox;
+    public CheckMenuItem optimizeCheckBox;
+    public Button startBtn;
+    public Button rwBtn;
+    public ToggleButton playBtn;
+    public Button ffBtn;
+    public Button endBtn;
+    public ToggleButton loopBtn;
+    public TimelineDisplay timelineDisplay;
     private Accordion settingsPanel;
     private double settingsLastWidth = -1;
     private int nodeCount = 0;
@@ -80,6 +99,8 @@
     private final ContentModel contentModel = Jfx3dViewerApp.getContentModel();
     private File loadedPath = null;
     private String[] supportedFormatRegex;
+    private TimelineController timelineController;
+    private SessionManager sessionManager = SessionManager.getSessionManager();
 
     @Override public void initialize(URL location, ResourceBundle resources) {
         try {
@@ -93,6 +114,10 @@
         } catch (IOException e) {
             e.printStackTrace();
         }
+        // create timelineController;
+        timelineController = new TimelineController(startBtn,rwBtn,playBtn,ffBtn,endBtn,loopBtn);
+        timelineController.timelineProperty().bind(contentModel.timelineProperty());
+        timelineDisplay.timelineProperty().bind(contentModel.timelineProperty());
         // listen for drops
         supportedFormatRegex = Importer3D.getSupportedFormatExtensionFilters();
         for (int i=0; i< supportedFormatRegex.length; i++) {
@@ -121,11 +146,9 @@
         contentModel.getSubScene().setOnDragDropped(
                 new EventHandler<DragEvent>() {
                     @Override public void handle(DragEvent event) {
-                        System.out.println("DROP event = " + event);
                         Dragboard db = event.getDragboard();
                         boolean success = false;
                         if (db.hasFiles()) {
-                            System.out.println("Dropped: " + db.getFiles());
                             File supportedFile = null;
                             fileLoop: for (File file : db.getFiles()) {
                                 for (String format : supportedFormatRegex) {
@@ -148,6 +171,15 @@
                         event.consume();
                     }
                 });
+
+        sessionManager.bind(optimizeCheckBox.selectedProperty(), "optimize");
+        sessionManager.bind(loadAsPolygonsCheckBox.selectedProperty(), "loadAsPolygons");
+        sessionManager.bind(loopBtn.selectedProperty(), "loop");
+
+        String url = sessionManager.getProperties().getProperty(Jfx3dViewerApp.FILE_URL_PROPERTY);
+        if (url == null) url = ContentModel.class.getResource("drop-here.obj").toExternalForm();
+        load(url);
+
         // do initial status update
         updateStatus();
     }
@@ -155,12 +187,11 @@
     public void open(ActionEvent actionEvent) {
         FileChooser chooser = new FileChooser();
         chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Supported files", Importer3D.getSupportedFormatExtensionFilters()));
-//        chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*"));
         if (loadedPath != null) {
             chooser.setInitialDirectory(loadedPath.getAbsoluteFile().getParentFile());
         }
         chooser.setTitle("Select file to load");
-        File newFile = chooser.showOpenDialog(openBtn.getScene().getWindow());
+        File newFile = chooser.showOpenDialog(openMenuBtn.getScene().getWindow());
         if (newFile != null) {
             load(newFile);
         }
@@ -169,10 +200,39 @@
     private void load(File file) {
         loadedPath = file;
         try {
-            Node content = Importer3D.load(file.toURI().toURL().toString());
-            contentModel.set3dContent(content);
-        } catch (IOException e) {
-            e.printStackTrace();
+            doLoad(file.toURI().toURL().toString());
+        } catch (MalformedURLException ex) {
+            Logger.getLogger(MainController.class.getName()).log(Level.SEVERE, null, ex);
+        }
+    }
+
+    private void load(String fileUrl) {
+        try {
+            loadedPath = new File(new URL(fileUrl).toURI()).getAbsoluteFile();
+            doLoad(fileUrl);
+        } catch (MalformedURLException | URISyntaxException ex) {
+            Logger.getLogger(MainController.class.getName()).log(Level.SEVERE, null, ex);
+        }
+    }
+
+    private void doLoad(String fileUrl) {
+        sessionManager.getProperties().setProperty(Jfx3dViewerApp.FILE_URL_PROPERTY, fileUrl);
+        try {
+            Pair<Node,Timeline> content = Importer3D.loadIncludingAnimation(
+                    fileUrl, loadAsPolygonsCheckBox.isSelected());
+            Timeline timeline = content.getValue();
+            if (optimizeCheckBox.isSelected()) {
+                new Optimizer(timeline,content.getKey()).optimize();
+            }
+            contentModel.set3dContent(content.getKey());
+            contentModel.setTimeline(timeline);
+
+            if (timeline != null) {
+                timeline.setCycleCount(Timeline.INDEFINITE);
+                timeline.play();
+            }
+        } catch (IOException ex) {
+            Logger.getLogger(MainController.class.getName()).log(Level.SEVERE, null, ex);
         }
         updateStatus();
     }
@@ -182,7 +242,8 @@
         meshCount = 0;
         triangleCount = 0;
         updateCount(contentModel.getRoot3D());
-        final Bounds bounds = contentModel.get3dContent().getBoundsInLocal();
+        Node content = contentModel.get3dContent();
+        final Bounds bounds = content == null ? new BoundingBox(0, 0, 0, 0) : content.getBoundsInLocal();
         status.setText(
                 String.format("Nodes [%d] :: Meshes [%d] :: Triangles [%d] :: " +
                                "Bounds [w=%.2f,h=%.2f,d=%.2f]",
@@ -237,10 +298,30 @@
         if (loadedPath != null) {
             chooser.setInitialDirectory(loadedPath.getAbsoluteFile().getParentFile());
         }
-        chooser.setTitle("Save fxml file");
-        File newFile = chooser.showSaveDialog(openBtn.getScene().getWindow());
+        chooser.getExtensionFilters().addAll(
+                new FileChooser.ExtensionFilter("FXML","*.fxml"),
+                new FileChooser.ExtensionFilter("Java Source","*.java")
+        );
+        chooser.setTitle("Export 3D Model");
+        File newFile = chooser.showSaveDialog(openMenuBtn.getScene().getWindow());
         if (newFile != null) {
-            new FXMLExporter(newFile.getAbsolutePath()).export(contentModel.get3dContent());
+            String extension = newFile.getName().substring(newFile.getName().lastIndexOf('.')+1,newFile.getName().length()).toLowerCase();
+            System.out.println("extension = " + extension);
+            if ("java".equals(extension)) {
+                final String url = contentModel.getLoadedUrl();
+                final String baseUrl = url.substring(0, url.lastIndexOf('/'));
+
+                JavaSourceExporter javaSourceExporter = new JavaSourceExporter(
+                        baseUrl,
+                        contentModel.get3dContent(),
+                        contentModel.getTimeline(),
+                        newFile);
+                javaSourceExporter.export();
+            } else if ("fxml".equals(extension)) {
+                new FXMLExporter(newFile.getAbsolutePath()).export(contentModel.get3dContent());
+            } else {
+                System.err.println("Can not export a file of type [."+extension+"]");
+            }
         }
     }
 }
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/OldTestViewer.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/OldTestViewer.java	Wed Jun 19 13:57:16 2013 -0700
@@ -35,14 +35,9 @@
 // import testviewer.Frame;
 
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.FileWriter;
 import java.io.IOException;
-import java.io.Reader;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javafx.animation.Timeline;
@@ -96,9 +91,8 @@
  *
  */
 public class OldTestViewer extends Application {
-    public static final String SESSION_PROPERTIES_FILENAME = "session.properties";
     public static final String PATH_PROPERTY = "file";
-    
+
     final private PointLight pointLight = new PointLight();
     final private PointLight pointLight2 = new PointLight();
     final private PointLight pointLight3 = new PointLight();
@@ -128,7 +122,7 @@
     double SHIFT_MULTIPLIER = 0.1;
     double ALT_MULTIPLIER = 0.5;
     
-    boolean enableSaveSession = true;
+    private SessionManager sessionManager;
     
     double mousePosX;
     double mousePosY;
@@ -469,7 +463,7 @@
             Logger.getLogger(OldTestViewer.class.getName()).log(Level.SEVERE, null, ex);
             return new MayaGroup();
         }
-        mayaImporter.load(url.toString());
+        mayaImporter.load(url.toString(), false);
         timeline = mayaImporter.getTimeline();
         timeline.setCycleCount(Timeline.INDEFINITE);
         //timeline.setAutoReverse(true);
@@ -1088,6 +1082,7 @@
     //=============================================================================
     @Override
     public void start(Stage primaryStage) {
+        sessionManager = SessionManager.createSessionManager("OldTestViewer");
         System.out.println("new File().getAbsolutePath() = " + new File("").getAbsolutePath());
 
         buildScene();
@@ -1108,11 +1103,21 @@
         primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
             @Override
             public void handle(WindowEvent event) {
-                saveSession();
+                if (loadedPath != null) {
+                    sessionManager.getProperties().setProperty(PATH_PROPERTY, loadedPath.getAbsolutePath());
+                }
+                sessionManager.saveSession();
             }
         });
         
-        loadSession();
+        sessionManager.loadSession();
+
+        String path = sessionManager.getProperties().getProperty(PATH_PROPERTY);
+        if (path != null) {
+            loadNewNode(new File(path));
+        }
+
+
         scene.setCamera(camera);
                 
         // ScenicView.show(scene);
@@ -1379,46 +1384,6 @@
         new FXMLExporter("output.fxml").export(loadedNode);
     }
 
-    private void loadSession() {
-        Reader reader = null;
-        try {
-            Properties props = new Properties();
-            reader = new FileReader(SESSION_PROPERTIES_FILENAME);
-            props.load(reader);
-            String path = props.getProperty(PATH_PROPERTY);
-            if (path != null) {
-                loadNewNode(new File(path));
-            }
-        } catch (FileNotFoundException ex) {
-            Logger.getLogger(OldTestViewer.class.getName()).log(Level.SEVERE, null, ex);
-        } catch (IOException ex) {
-            Logger.getLogger(OldTestViewer.class.getName()).log(Level.SEVERE, null, ex);
-        } finally {
-            try {
-                if (reader != null) {
-                    reader.close();
-                }
-            } catch (IOException ex) {
-                Logger.getLogger(OldTestViewer.class.getName()).
-                        log(Level.SEVERE, null, ex);
-            }
-        }
-    }
-    
-    private void saveSession() {
-        if (enableSaveSession) {
-            Properties props = new Properties();
-            if (loadedPath != null) {
-                props.setProperty(PATH_PROPERTY, loadedPath.getAbsolutePath());
-            }
-            try {
-                props.store(new FileWriter(SESSION_PROPERTIES_FILENAME), "Jfx3dViewerApp session properties");
-            } catch (IOException ex) {
-                Logger.getLogger(OldTestViewer.class.getName()).
-                        log(Level.SEVERE, null, ex);
-            }
-        }
-    }
     /**
      * The main() method is ignored in correctly deployed JavaFX application.
      * main() serves only as fallback in case the application can not be
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SessionManager.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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 General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.javafx.experiments.jfx3dviewer;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.Toggle;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.paint.Color;
+
+public class SessionManager {
+
+    public final String SESSION_PROPERTIES_FILENAME;
+    private static final boolean ENABLE_SAVE_SESSION = true;
+    private String name;
+    private Properties props = new Properties();
+
+    private SessionManager(String name) {
+        this.name = name;
+        SESSION_PROPERTIES_FILENAME = name + "_session.properties";
+    }
+
+    private static SessionManager sessionManager;
+
+    public static SessionManager createSessionManager(String name) {
+        return sessionManager = new SessionManager(name);
+    }
+    
+    public static SessionManager getSessionManager() {
+        return sessionManager;
+    }
+
+    public Properties getProperties() {
+        return props;
+    }
+
+    public void loadSession() {
+        Reader reader = null;
+        try {
+            reader = new FileReader(SESSION_PROPERTIES_FILENAME);
+            props.load(reader);
+        } catch (FileNotFoundException ignored) {
+        } catch (IOException ex) {
+            Logger.getLogger(OldTestViewer.class.getName()).log(Level.SEVERE, null, ex);
+        } finally {
+            try {
+                if (reader != null) {
+                    reader.close();
+                }
+            } catch (IOException ex) {
+                Logger.getLogger(OldTestViewer.class.getName()).
+                        log(Level.SEVERE, null, ex);
+            }
+        }
+    }
+
+    public void saveSession() {
+        if (ENABLE_SAVE_SESSION) {
+            try {
+                props.store(new FileWriter(SESSION_PROPERTIES_FILENAME), name + " session properties");
+            } catch (IOException ex) {
+                Logger.getLogger(OldTestViewer.class.getName()).
+                        log(Level.SEVERE, null, ex);
+            }
+        }
+    }
+
+    public void bind(final BooleanProperty property, final String propertyName) {
+        String value = props.getProperty(propertyName);
+        if (value != null) property.set(Boolean.valueOf(value));
+        property.addListener(new InvalidationListener() {
+
+            @Override
+            public void invalidated(Observable o) {
+                props.setProperty(propertyName, property.getValue().toString());
+            }
+        });
+    }
+
+    public void bind(final ObjectProperty<Color> property, final String propertyName) {
+        String value = props.getProperty(propertyName);
+        if (value != null) property.set(Color.valueOf(value));
+        property.addListener(new InvalidationListener() {
+
+            @Override
+            public void invalidated(Observable o) {
+                props.setProperty(propertyName, property.getValue().toString());
+            }
+        });
+    }
+
+    public void bind(final DoubleProperty property, final String propertyName) {
+        String value = props.getProperty(propertyName);
+        if (value != null) property.set(Double.valueOf(value));
+        property.addListener(new InvalidationListener() {
+
+            @Override
+            public void invalidated(Observable o) {
+                props.setProperty(propertyName, property.getValue().toString());
+            }
+        });
+    }
+
+    public void bind(final ToggleGroup toggleGroup, final String propertyName) {
+        String selectedToggle = props.getProperty(propertyName);
+        for (Toggle t : toggleGroup.getToggles()) {
+            if (t.getUserData().equals(selectedToggle)) {
+                if (toggleGroup.getSelectedToggle() != t) {
+                    toggleGroup.selectToggle(t);
+                }
+                break;
+            }
+        }
+        toggleGroup.selectedToggleProperty().addListener(new InvalidationListener() {
+        
+            @Override
+            public void invalidated(Observable o) {
+                if (toggleGroup.getSelectedToggle() == null) {
+                    props.remove(propertyName);
+                } else {
+                    props.setProperty(propertyName, toggleGroup.getSelectedToggle().getUserData().toString());
+                }
+            }
+        });
+    }
+
+    public void bind(final Accordion accordion, final String propertyName) {
+        Object selectedPane = props.getProperty(propertyName);
+        for (TitledPane tp : accordion.getPanes()) {
+            if (tp.getText().equals(selectedPane)) {
+                accordion.setExpandedPane(tp);
+                break;
+            }
+        }
+        accordion.expandedPaneProperty().addListener(new InvalidationListener() {
+
+            @Override
+            public void invalidated(Observable o) {
+//                props.setProperty(propertyName, accordion.getExpandedPane().getText());
+            }
+        });
+    }
+
+}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SettingsController.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SettingsController.java	Wed Jun 19 13:57:16 2013 -0700
@@ -38,8 +38,6 @@
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.fxml.Initializable;
-import javafx.scene.Node;
-import javafx.scene.Parent;
 import javafx.scene.control.Accordion;
 import javafx.scene.control.CheckBox;
 import javafx.scene.control.ColorPicker;
@@ -48,10 +46,7 @@
 import javafx.scene.control.Toggle;
 import javafx.scene.control.ToggleGroup;
 import javafx.scene.paint.Color;
-import javafx.scene.shape.DrawMode;
-import javafx.scene.shape.MeshView;
-import com.javafx.experiments.shape3d.PolygonMesh;
-import com.javafx.experiments.shape3d.PolygonMeshView;
+import com.javafx.experiments.shape3d.SubDivision;
 
 /**
  * Controller class for settings panel
@@ -84,8 +79,10 @@
     public Slider light3y;
     public Slider light3z;
     public CheckBox wireFrameCheckbox;
-    public ToggleGroup subdivideGroup;
-
+    public ToggleGroup subdivisionLevelGroup;
+    public ToggleGroup subdivisionBoundaryGroup;
+    public ToggleGroup subdivisionSmoothGroup;
+    
     @Override public void initialize(URL location, ResourceBundle resources) {
         // keep one pane open always
         settings.expandedPaneProperty().addListener(new ChangeListener<TitledPane>() {
@@ -110,9 +107,34 @@
                 contentModel.setWireFrame(isWireframe);
             }
         });
-        subdivideGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
+        subdivisionLevelGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
             @Override public void changed(ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle selectedToggle) {
-                contentModel.setSubdivision(Integer.parseInt((String)selectedToggle.getUserData()));
+                if (selectedToggle == null && oldValue != null) {
+                    subdivisionLevelGroup.selectToggle(oldValue);
+                    selectedToggle = oldValue;
+                } else {
+                    contentModel.setSubdivisionLevel(Integer.parseInt((String)selectedToggle.getUserData()));
+                }
+            }
+        });
+        subdivisionBoundaryGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
+            @Override public void changed(ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle selectedToggle) {
+                if (selectedToggle == null && oldValue != null) {
+                    subdivisionBoundaryGroup.selectToggle(oldValue);
+                    selectedToggle = oldValue;
+                } else {
+                    contentModel.setBoundaryMode((SubDivision.BoundaryMode) selectedToggle.getUserData());
+                }
+            }
+        });
+        subdivisionSmoothGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
+            @Override public void changed(ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle selectedToggle) {
+                if (selectedToggle == null && oldValue != null) {
+                    subdivisionSmoothGroup.selectToggle(oldValue);
+                    selectedToggle = oldValue;
+                } else {
+                    contentModel.setMapBorderMode((SubDivision.MapBorderMode) selectedToggle.getUserData());
+                }
             }
         });
         // wire up settings in LIGHTS
@@ -176,6 +198,37 @@
         // wire up settings in CAMERA
         fovSlider.setValue(contentModel.getCamera().getFieldOfView());
         contentModel.getCamera().fieldOfViewProperty().bind(fovSlider.valueProperty());
+
+        SessionManager sessionManager = SessionManager.getSessionManager();
+
+        sessionManager.bind(showAxisCheckBox.selectedProperty(), "showAxis");
+        sessionManager.bind(yUpCheckBox.selectedProperty(), "yUp");
+        sessionManager.bind(scaleToFitCheckBox.selectedProperty(), "scaleToFit");
+        sessionManager.bind(wireFrameCheckbox.selectedProperty(), "wireFrame");
+        sessionManager.bind(backgroundColorPicker.valueProperty(), "backgroundColor");
+        sessionManager.bind(fovSlider.valueProperty(), "fieldOfView");
+        sessionManager.bind(subdivisionLevelGroup, "subdivisionLevel");
+        sessionManager.bind(subdivisionBoundaryGroup, "subdivisionBoundary");
+        sessionManager.bind(subdivisionSmoothGroup, "subdivisionSmooth");
+        sessionManager.bind(light1ColorPicker.valueProperty(), "light1Color");
+        sessionManager.bind(light1EnabledCheckBox.selectedProperty(), "light1Enabled");
+        sessionManager.bind(light1followCameraCheckBox.selectedProperty(), "light1FollowCamera");
+        sessionManager.bind(light1x.valueProperty(), "light1X");
+        sessionManager.bind(light1y.valueProperty(), "light1Y");
+        sessionManager.bind(light1z.valueProperty(), "light1Z");
+        sessionManager.bind(light2ColorPicker.valueProperty(), "light2Color");
+        sessionManager.bind(light2EnabledCheckBox.selectedProperty(), "light2Enabled");
+        sessionManager.bind(light2x.valueProperty(), "light2X");
+        sessionManager.bind(light2y.valueProperty(), "light2Y");
+        sessionManager.bind(light2z.valueProperty(), "light2Z");
+        sessionManager.bind(light3ColorPicker.valueProperty(), "light3Color");
+        sessionManager.bind(light3EnabledCheckBox.selectedProperty(), "light3Enabled");
+        sessionManager.bind(light3x.valueProperty(), "light3X");
+        sessionManager.bind(light3y.valueProperty(), "light3Y");
+        sessionManager.bind(light3z.valueProperty(), "light3Z");
+        sessionManager.bind(ambientColorPicker.valueProperty(), "ambient");
+        sessionManager.bind(ambientEnableCheckbox.selectedProperty(), "ambientEnable");
+        sessionManager.bind(settings, "settingsPane");
     }
 
 
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SimpleViewerApp.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/SimpleViewerApp.java	Wed Jun 19 13:57:16 2013 -0700
@@ -48,6 +48,7 @@
 import javafx.scene.transform.Rotate;
 import javafx.scene.transform.Translate;
 import javafx.stage.Stage;
+import javafx.stage.StageStyle;
 import javafx.util.Duration;
 import com.javafx.experiments.importers.Importer3D;
 import com.sun.javafx.perf.PerformanceTracker;
@@ -67,8 +68,9 @@
 
     @Override public void start(Stage stage) throws Exception {
         List<String> args = getParameters().getRaw();
-        Scene scene = new Scene(root3D,1920,1080,true,false);
-        scene.setFill(Color.ALICEBLUE);
+        final Scene scene = new Scene(root3D,1920,1080,true,false);
+        scene.setFill(Color.TRANSPARENT);
+        stage.initStyle(StageStyle.TRANSPARENT);
 
         // CAMERA
         camera.getTransforms().addAll(
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/TimelineController.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.javafx.experiments.jfx3dviewer;
+
+import javafx.animation.Timeline;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.Button;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.input.MouseEvent;
+import javafx.util.Duration;
+
+/**
+ * A controller class to bind up a timeline to play controler buttons
+ */
+public class TimelineController {
+    private final ChangeListener<Number> rateListener = new ChangeListener<Number>() {
+        @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newRate) {
+            System.out.println("newRate = " + newRate);
+            if (newRate.intValue() == 0 && playBtn.isSelected()) {
+                playBtn.setSelected(false);
+            }
+        }
+    };
+    private final SimpleObjectProperty<Timeline> timeline = new SimpleObjectProperty<Timeline>() {
+        private Timeline old;
+        @Override protected void invalidated() {
+            Timeline t = get();
+            if (old != null) {
+                old.currentRateProperty().removeListener(rateListener);
+            }
+            if (t == null) {
+                startBtn.setDisable(true);
+                rwBtn.setDisable(true);
+                playBtn.setDisable(true);
+                ffBtn.setDisable(true);
+                endBtn.setDisable(true);
+                loopBtn.setDisable(true);
+            } else {
+                startBtn.setDisable(false);
+                rwBtn.setDisable(false);
+                playBtn.setDisable(false);
+                ffBtn.setDisable(false);
+                endBtn.setDisable(false);
+                loopBtn.setDisable(false);
+                playBtn.setSelected(t.getCurrentRate() != 0);
+                loopBtn.setSelected(t.getCycleDuration().equals(Timeline.INDEFINITE));
+                t.currentRateProperty().addListener(rateListener);
+            }
+            old = t;
+        }
+    };
+    public Timeline getTimeline() { return timeline.get(); }
+    public SimpleObjectProperty<Timeline> timelineProperty() { return timeline; }
+    public void setTimeline(Timeline timeline) { this.timeline.set(timeline); }
+
+    private final Button startBtn;
+    private final Button rwBtn;
+    private final ToggleButton playBtn;
+    private final Button ffBtn;
+    private final Button endBtn;
+    private final ToggleButton loopBtn;
+
+    public TimelineController(Button startBtn, Button rwBtn,final ToggleButton playBtn, Button ffBtn, Button endBtn,final ToggleButton loopBtn) {
+        this.startBtn = startBtn;
+        this.rwBtn = rwBtn;
+        this.playBtn = playBtn;
+        this.ffBtn = ffBtn;
+        this.endBtn = endBtn;
+        this.loopBtn = loopBtn;
+
+        this.startBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                getTimeline().jumpTo(Duration.ZERO);
+                getTimeline().pause();
+            }
+        });
+        this.endBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                getTimeline().jumpTo(getTimeline().getTotalDuration());
+                getTimeline().pause();
+            }
+        });
+        this.playBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                System.out.println("playBtn.isSelected() = " + playBtn.isSelected());
+                if (playBtn.isSelected()) { // currently paused so play
+                    getTimeline().play();
+                } else { // currently playing so pause
+                    getTimeline().pause();
+                }
+            }
+        });
+        this.ffBtn.setOnMousePressed(
+                new EventHandler<MouseEvent>() {
+                    @Override public void handle(MouseEvent event) {
+                        getTimeline().setRate(2);
+                    }
+                });
+        this.ffBtn.setOnMouseReleased(
+                new EventHandler<MouseEvent>() {
+                    @Override public void handle(MouseEvent event) {
+                        getTimeline().setRate(1);
+                    }
+                });
+        this.rwBtn.setOnMousePressed(
+                new EventHandler<MouseEvent>() {
+                    @Override public void handle(MouseEvent event) {
+                        getTimeline().setRate(-2);
+                    }
+                });
+        this.rwBtn.setOnMouseReleased(
+                new EventHandler<MouseEvent>() {
+                    @Override public void handle(MouseEvent event) {
+                        getTimeline().setRate(1);
+                    }
+                });
+        this.loopBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                System.out.println("LOOP CHANGE TO "+loopBtn.isSelected()+"  before="+getTimeline().getCycleCount());
+                if (loopBtn.isSelected()) {
+                    getTimeline().stop();
+                    getTimeline().setCycleCount(Timeline.INDEFINITE);
+                    getTimeline().play();
+                } else {
+                    getTimeline().stop();
+                    getTimeline().setCycleCount(1);
+                    getTimeline().play();
+                }
+                System.out.println("    after = "+getTimeline().getCycleCount());
+            }
+        });
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/TimelineDisplay.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.javafx.experiments.jfx3dviewer;
+
+import javafx.animation.Timeline;
+import javafx.beans.binding.DoubleBinding;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.layout.Region;
+import javafx.scene.text.Text;
+
+/**
+ * Visual display for timeline play head and length
+ */
+public class TimelineDisplay extends Region {
+    private SimpleDoubleProperty currentTimeAsPercentage = new SimpleDoubleProperty(0) {
+        @Override protected void invalidated() {
+            requestLayout();
+        }
+    };
+    private final SimpleObjectProperty<Timeline> timeline = new SimpleObjectProperty<Timeline>() {
+        private Timeline old;
+        @Override protected void invalidated() {
+            final Timeline t = get();
+            if (old != null) {
+                currentTimeAsPercentage.unbind();
+                end.textProperty().unbind();
+            }
+            if (t == null) {
+                setVisible(false);
+            } else {
+                setVisible(true);
+                currentTimeAsPercentage.bind(
+                        new DoubleBinding() {
+                            { bind(t.currentTimeProperty(), t.cycleDurationProperty()); }
+
+                            @Override protected double computeValue() {
+                                return t.getCurrentTime().toMillis() / t.getCycleDuration().toMillis();
+                            }
+                        });
+                end.textProperty().bind(
+                        new StringBinding() {
+                            { bind(t.cycleDurationProperty()); }
+
+                            @Override protected String computeValue() {
+                                return String.format("%.2fs", t.getCycleDuration().toSeconds());
+                            }
+                        });
+                current.textProperty().bind(
+                        new StringBinding() {
+                            { bind(t.currentTimeProperty()); }
+
+                            @Override protected String computeValue() {
+                                return String.format("%.2fs", t.getCurrentTime().toSeconds());
+                            }
+                        });
+            }
+            old = t;
+        }
+    };
+    public Timeline getTimeline() { return timeline.get(); }
+    public SimpleObjectProperty<Timeline> timelineProperty() { return timeline; }
+    public void setTimeline(Timeline timeline) { this.timeline.set(timeline); }
+
+    private final Region background = new Region();
+    private final Region bar = new Region();
+    private final Region progress = new Region();
+    private final Text start = new Text("0s");
+    private final Text end = new Text();
+    private final Text current = new Text();
+
+    public TimelineDisplay() {
+        getStyleClass().add("timeline-display");
+        background.getStyleClass().add("background");
+        background.setCache(true); // cache so we don't have to render shadow every frame
+        bar.getStyleClass().add("bar");
+        progress.getStyleClass().add("progress");
+        getChildren().addAll(background,start,current,end,bar,progress);
+    }
+
+    @Override protected double computePrefWidth(double height) {
+        return 200;
+    }
+
+    @Override protected double computePrefHeight(double width) {
+        return 24;
+    }
+
+    @Override protected void layoutChildren() {
+        final double w = getWidth() - snappedLeftInset() - snappedRightInset();
+        background.resizeRelocate(0,0,getWidth(),getHeight());
+        bar.resizeRelocate(snappedLeftInset(),snappedTopInset(),w,6);
+        progress.resizeRelocate(snappedLeftInset(),snappedTopInset(),w*currentTimeAsPercentage.get(),6);
+        start.setLayoutX(snappedLeftInset());
+        start.setLayoutY(getHeight() - snappedBottomInset());
+        current.setLayoutX((int)((getWidth() - current.getLayoutBounds().getWidth())/2d));
+        current.setLayoutY(getHeight() - snappedBottomInset());
+        end.setLayoutX(getWidth() - snappedRightInset() - end.getLayoutBounds().getWidth());
+        end.setLayoutY(getHeight() - snappedBottomInset());
+    }
+}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/main.fxml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/main.fxml	Wed Jun 19 13:57:16 2013 -0700
@@ -34,18 +34,64 @@
 <?import java.lang.*?>
 <?import java.net.*?>
 <?import java.util.*?>
+<?import javafx.geometry.*?>
 <?import javafx.scene.control.*?>
 <?import javafx.scene.control.MenuBar?>
 <?import javafx.scene.control.ToolBar?>
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.paint.*?>
+<?import com.javafx.experiments.jfx3dviewer.*?>
 
 <VBox id="root" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml" fx:controller="com.javafx.experiments.jfx3dviewer.MainController">
   <ToolBar>
     <items>
-      <Button fx:id="openBtn" mnemonicParsing="false" onAction="#open" text="Open..." />
-      <Button mnemonicParsing="false" text="Export..."  onAction="#export" />
-      <Pane maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS" prefHeight="-1.0" prefWidth="-1.0" />
+      <SplitMenuButton fx:id="openMenuBtn" mnemonicParsing="false" onAction="#open" text="Open...">
+        <items>
+          <CheckMenuItem mnemonicParsing="false" text="Load as Polygons" fx:id="loadAsPolygonsCheckBox" />
+          <CheckMenuItem mnemonicParsing="false" text="Optimize" fx:id="optimizeCheckBox" />
+        </items>
+      </SplitMenuButton>
+      <Button mnemonicParsing="false" onAction="#export" text="Export..." />
+      <!--HBox.hgrow="ALWAYS"-->
+      <Pane maxWidth="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="ALWAYS"/>
+      <TimelineDisplay fx:id="timelineDisplay" />
+      <HBox id="playControls" prefHeight="-1.0" prefWidth="-1.0">
+        <children>
+          <Button fx:id="startBtn" mnemonicParsing="false" styleClass="left-pill" text="|&lt;">
+            <graphic>
+              <Pane prefHeight="16.0" prefWidth="16.0" />
+            </graphic>
+          </Button>
+          <Button fx:id="rwBtn" mnemonicParsing="false" styleClass="center-pill" text="&lt;&lt;">
+            <graphic>
+              <Pane prefHeight="16.0" prefWidth="16.0" />
+            </graphic>
+          </Button>
+          <ToggleButton fx:id="playBtn" mnemonicParsing="false" styleClass="center-pill" text="&gt;">
+            <graphic>
+              <Pane prefHeight="16.0" prefWidth="16.0" />
+            </graphic>
+          </ToggleButton>
+          <Button fx:id="ffBtn" mnemonicParsing="false" styleClass="center-pill" text="&gt;&gt;">
+            <graphic>
+              <Pane prefHeight="16.0" prefWidth="16.0" />
+            </graphic>
+          </Button>
+          <Button fx:id="endBtn" mnemonicParsing="false" styleClass="center-pill" text="&gt;|">
+            <graphic>
+              <Pane prefHeight="16.0" prefWidth="16.0" />
+            </graphic>
+          </Button>
+          <ToggleButton fx:id="loopBtn" mnemonicParsing="false" selected="true" styleClass="right-pill" text="R">
+            <graphic>
+              <Pane prefHeight="16.0" prefWidth="16.0" />
+            </graphic>
+          </ToggleButton>
+        </children>
+        <padding>
+          <Insets right="5.0" />
+        </padding>
+      </HBox>
       <ToggleButton fx:id="settingsBtn" mnemonicParsing="false" onAction="#toggleSettings" text="Settings" />
     </items>
   </ToolBar>
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/settings.fxml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/settings.fxml	Wed Jun 19 13:57:16 2013 -0700
@@ -40,6 +40,7 @@
 <?import javafx.scene.control.ToolBar?>
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.paint.*?>
+<?import com.javafx.experiments.shape3d.*?>
 <?scenebuilder-stylesheet viewer.css?>
 <!--suppress JavaFxUnresolvedStyleClassReference -->
 
@@ -59,20 +60,76 @@
             <ColorPicker fx:id="backgroundColorPicker" GridPane.columnIndex="1" GridPane.rowIndex="3" />
             <Label text="Wireframe" GridPane.columnIndex="0" GridPane.rowIndex="4" />
             <CheckBox fx:id="wireFrameCheckbox" mnemonicParsing="false" text="" GridPane.columnIndex="1" GridPane.rowIndex="4" />
-            <Label text="Subdivide" GridPane.columnIndex="0" GridPane.rowIndex="5" />
+            <Label text="Subdivision level" GridPane.columnIndex="0" GridPane.rowIndex="5" />
             <HBox prefHeight="-1.0" prefWidth="-1.0" GridPane.columnIndex="1" GridPane.rowIndex="5">
               <children>
                 <ToggleButton mnemonicParsing="false" selected="true" styleClass="left-pill" text="None" userData="0">
                   <toggleGroup>
-                    <ToggleGroup fx:id="subdivideGroup" />
+                    <ToggleGroup fx:id="subdivisionLevelGroup" />
                   </toggleGroup>
                 </ToggleButton>
-                <ToggleButton mnemonicParsing="false" styleClass="center-pill" text="1" toggleGroup="$subdivideGroup" userData="1" />
-                <ToggleButton mnemonicParsing="false" styleClass="center-pill" text="2" toggleGroup="$subdivideGroup" userData="2" />
-                <ToggleButton mnemonicParsing="false" styleClass="right-pill" text="3" toggleGroup="$subdivideGroup" userData="3" />
+                <ToggleButton mnemonicParsing="false" styleClass="center-pill" text="1" toggleGroup="$subdivisionLevelGroup" userData="1" />
+                <ToggleButton mnemonicParsing="false" styleClass="center-pill" text="2" toggleGroup="$subdivisionLevelGroup" userData="2" />
+                <ToggleButton mnemonicParsing="false" styleClass="right-pill" text="3" toggleGroup="$subdivisionLevelGroup" userData="3" />
               </children>
             </HBox>
-            <Region maxHeight="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" GridPane.columnIndex="0" GridPane.rowIndex="6" />
+            <Label text="Creases (Boundary Rules)" GridPane.columnIndex="0" GridPane.rowIndex="6" />
+            <HBox prefHeight="-1.0" prefWidth="-1.0" GridPane.columnIndex="1" GridPane.rowIndex="6">
+              <children>
+                <ToggleButton mnemonicParsing="false" selected="true" styleClass="left-pill" text="Edges">
+                  <toggleGroup>
+                    <ToggleGroup fx:id="subdivisionBoundaryGroup" />
+                  </toggleGroup>
+                  <userData>
+                    <SubDivision.BoundaryMode fx:value="CREASE_EDGES"/>
+                  </userData>
+                  <tooltip>
+                    <Tooltip text="Only edges at the boundary are treated as creases" />
+                  </tooltip>
+                </ToggleButton>
+                <ToggleButton mnemonicParsing="false" styleClass="right-pill" text="All" toggleGroup="$subdivisionBoundaryGroup">
+                  <userData>
+                    <SubDivision.BoundaryMode fx:value="CREASE_ALL"/>
+                  </userData>
+                  <tooltip>
+                    <Tooltip text="Edges and points at the boundary are treated as creases" />
+                  </tooltip>
+                </ToggleButton>
+              </children>
+            </HBox>
+            <Label text="Smooth Texture Map" GridPane.columnIndex="0" GridPane.rowIndex="7" />
+            <HBox prefHeight="-1.0" prefWidth="-1.0" GridPane.columnIndex="1" GridPane.rowIndex="7">
+              <children>
+                <ToggleButton mnemonicParsing="false" selected="true" styleClass="left-pill" text="None">
+                  <toggleGroup>
+                    <ToggleGroup fx:id="subdivisionSmoothGroup" />
+                  </toggleGroup>
+                  <userData>
+                    <SubDivision.MapBorderMode fx:value="NOT_SMOOTH"/>
+                  </userData>
+                  <tooltip>
+                    <Tooltip text="Keeps the same uvs for all control points" />
+                  </tooltip>
+                </ToggleButton>
+                <ToggleButton mnemonicParsing="false" styleClass="center-pill" text="Internal" toggleGroup="$subdivisionSmoothGroup">
+                  <userData>
+                    <SubDivision.MapBorderMode fx:value="SMOOTH_INTERNAL"/>
+                  </userData>
+                  <tooltip>
+                    <Tooltip text="Smooths uvs of points at corners" />
+                  </tooltip>
+                </ToggleButton>
+                <ToggleButton mnemonicParsing="false" styleClass="right-pill" text="All" toggleGroup="$subdivisionSmoothGroup">
+                  <userData>
+                    <SubDivision.MapBorderMode fx:value="SMOOTH_ALL"/>
+                  </userData>
+                  <tooltip>
+                    <Tooltip text="Smooths uvs of points at boundaries" />
+                  </tooltip>
+                </ToggleButton>
+              </children>
+            </HBox>
+            <Region maxHeight="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" GridPane.columnIndex="0" GridPane.rowIndex="8" />
           </children>
           <columnConstraints>
             <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
@@ -88,6 +145,8 @@
             <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
             <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
             <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
+            <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
+            <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
             <RowConstraints minHeight="10.0" vgrow="ALWAYS" />
           </rowConstraints>
         </GridPane>
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/viewer.css	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/jfx3dviewer/viewer.css	Wed Jun 19 13:57:16 2013 -0700
@@ -169,4 +169,52 @@
         linear-gradient(to bottom, derive(-fx-default-button, -12%) 0%, derive(-fx-default-button, -3%) 20%, derive(-fx-default-button, -1%) 50%);
     -fx-background-insets: -0.2, 1, 2, 1 32 1 1, 1 33 1 1;
     -fx-background-radius: 3px, 2px, 1px, 2px 0 0 2px, 1 0 0 1;
+}
+#playControls * {
+    -fx-content-display: graphic-only;
+}
+#playControls > * > Pane {
+    -fx-background-color: white,#666;
+    -fx-background-insets: 1 0 -1 0, 0;
+}
+#playControls > #startBtn > Pane {
+    -fx-shape: "M27.05,532h-0.782c-0.333,0-0.602,0.273-0.602,0.61v14.659c0,0.338,0.269,0.61,0.602,0.61h0.782c0.332,0,0.602-0.272,0.602-0.61V532.61C27.652,532.273,27.382,532,27.05,532zM38.9,532l-9.263,7.94l9.263,7.939V532z";
+}
+#playControls > #rwBtn > Pane {
+    -fx-shape: "M64.866,531.999l-8.818,7.965l8.818,7.964v-6.796l7.301,6.796v-15.929l-7.301,6.797V531.999z";
+}
+#playControls > #playBtn:selected  > Pane {
+    -fx-shape: "M20.716,531.999c-0.665,0-1.204,0.542-1.204,1.212v13.328c0,0.671,0.539,1.213,1.204,1.213h2.188c0.665,0,1.203-0.542,1.203-1.213v-13.328c0-0.67-0.538-1.212-1.203-1.212H20.716zM10.87,531.999c-0.665,0-1.204,0.542-1.204,1.212v13.328c0,0.671,0.539,1.213,1.204,1.213h2.188c0.665,0,1.203-0.542,1.203-1.213v-13.328c0-0.67-0.539-1.212-1.203-1.212H10.87z";
+}
+#playControls > #playBtn> Pane {
+    -fx-shape: "M0,531.999l8.667,8L0,548V531.999z";
+}
+#playControls > #ffBtn > Pane {
+    -fx-shape: "M82.09,532v6.606L74.833,532v15.832l7.257-6.606v6.606l8.576-7.916L82.09,532z";
+}
+#playControls > #endBtn > Pane {
+    -fx-shape: "M52.525,532c-0.333,0-0.602,0.273-0.602,0.611v14.669c0,0.337,0.27,0.611,0.602,0.611h0.782c0.333,0,0.602-0.274,0.602-0.611v-14.669c0-0.338-0.27-0.611-0.602-0.611H52.525zM40.667,532v15.892l9.27-7.946L40.667,532z";
+}
+.tool-bar #loopBtn Pane {
+    -fx-shape: "M103.31,539.442v-2.544h-6.487c-0.907,0-1.646,0.738-1.646,1.646v1.637l-2.512,2.526v-4.163c0-2.296,1.861-4.157,4.157-4.157h6.487v-2.392l4.034,3.724L103.31,539.442z M92.753,544.033l4.034,3.725v-2.393h6.487c2.296,0,4.157-1.861,4.157-4.157v-4.163l-2.512,2.526v1.637c0,0.907-0.738,1.646-1.646,1.646h-6.487v-2.544L92.753,544.033z";
+}
+.timeline-display {
+    -fx-font-size: 10px;
+    -fx-font-weight: bold;
+    -fx-padding: 6 6 4 6;
+}
+.timeline-display .background {
+    -fx-background-color: white,#b8c3a6;
+    -fx-background-insets: 1 0 -1 0,0;
+    -fx-background-radius: 6, 6;
+    -fx-effect: innershadow(gaussian, #52574abb, 5, 0, 0, 2);
+}
+.timeline-display Text {
+    -fx-fill: #52574a;
+}
+.timeline-display .bar {
+    -fx-border-color: #52574a;
+}
+.timeline-display .progress {
+    -fx-background-color: #52574a;
 }
\ No newline at end of file
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/PolygonMesh.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/PolygonMesh.java	Wed Jun 19 13:57:16 2013 -0700
@@ -1,20 +1,39 @@
 package com.javafx.experiments.shape3d;
 
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableFloatArray;
+
 /**
  * A Mesh where each face can be a Polygon
  *
  * TODO convert to using ObservableFloatArray and ObservableIntegerArray
  */
 public class PolygonMesh {
-    public float[] points;
+    private final ObservableFloatArray points = FXCollections.observableFloatArray();
     public float[] texCoords;
     public int[][] faces;
+    private int numEdgesInFaces = -1;
 
     public PolygonMesh() {}
 
     public PolygonMesh(float[] points,float[] texCoords, int[][] faces) {
-        this.points = points;
+        this.points.addAll(points);
         this.texCoords = texCoords;
         this.faces = faces;
     }
+
+    public ObservableFloatArray getPoints() {
+        return points;
+    }
+    
+    public int getNumEdgesInFaces() {
+        if (numEdgesInFaces == -1) {
+            numEdgesInFaces = 0;
+            for(int[] face : faces) {
+                numEdgesInFaces += face.length;
+            }
+           numEdgesInFaces /= 2;
+        }
+        return numEdgesInFaces;
+    }
 }
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/PolygonMeshView.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/PolygonMeshView.java	Wed Jun 19 13:57:16 2013 -0700
@@ -4,6 +4,7 @@
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleIntegerProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ArrayChangeListener;
 import javafx.collections.ObservableFloatArray;
 import javafx.collections.ObservableIntegerArray;
 import javafx.scene.Parent;
@@ -20,6 +21,7 @@
     private static final boolean DEBUG = false;
     private final MeshView meshView = new MeshView();
     private PolygonMesh subdividedMesh = null;
+    // TODO keep only one TriangleMesh around
 
     // =========================================================================
     // PROPERTIES
@@ -35,9 +37,19 @@
         }
     };
     public PolygonMesh getMesh() { return mesh.get(); }
-    public void setMesh(PolygonMesh mesh) { this.mesh.set(mesh); }
+    public void setMesh(PolygonMesh mesh) { 
+        this.mesh.set(mesh);
+        mesh.getPoints().addListener(new ArrayChangeListener<ObservableFloatArray>() {
+            @Override
+            public void onChanged(ObservableFloatArray t, boolean bln, int i, int i1) {
+                // TODO don't update the whole mesh, only update the parts affected by points
+                updateMesh();
+            }
+        });
+    }
+    
     public ObjectProperty<PolygonMesh> meshProperty() { return mesh; }
-
+    
     /**
      * Defines the drawMode this {@code Shape3D}.
      *
@@ -94,14 +106,42 @@
      *
      * @defaultValue 0
      */
-    private SimpleIntegerProperty subdivision = new SimpleIntegerProperty(0) {
+    private SimpleIntegerProperty subdivisionLevel = new SimpleIntegerProperty(0) {
         @Override protected void invalidated() {
             updateSubdivision();
         }
     };
-    public int getSubdivision() { return subdivision.get(); }
-    public SimpleIntegerProperty subdivisionProperty() { return subdivision; }
-    public void setSubdivision(int subdivision) { this.subdivision.set(subdivision); }
+    public int getSubdivisionLevel() { return subdivisionLevel.get(); }
+    public SimpleIntegerProperty subdivisionLevelProperty() { return subdivisionLevel; }
+    public void setSubdivisionLevel(int subdivisionLevel) { this.subdivisionLevel.set(subdivisionLevel); }
+    
+    /**
+     * Texture mapping boundary rule for Catmull Clark subdivision applied to the mesh
+     *
+     * @defaultValue BoundaryMode.CREASE_EDGES
+     */
+    private SimpleObjectProperty<SubDivision.BoundaryMode> boundaryMode = new SimpleObjectProperty<SubDivision.BoundaryMode>(SubDivision.BoundaryMode.CREASE_EDGES) {
+        @Override protected void invalidated() {
+            updateSubdivision();
+        }
+    };
+    public SubDivision.BoundaryMode getBoundaryMode() { return boundaryMode.get(); }
+    public SimpleObjectProperty<SubDivision.BoundaryMode> boundaryModeProperty() { return boundaryMode; }
+    public void setBoundaryMode(SubDivision.BoundaryMode boundaryMode) { this.boundaryMode.set(boundaryMode); }
+    
+    /**
+     * Texture mapping smoothness option for Catmull Clark subdivision applied to the mesh
+     *
+     * @defaultValue MapBorderMode.NOT_SMOOTH
+     */
+    private SimpleObjectProperty<SubDivision.MapBorderMode> mapBorderMode = new SimpleObjectProperty<SubDivision.MapBorderMode>(SubDivision.MapBorderMode.NOT_SMOOTH) {
+        @Override protected void invalidated() {
+            updateSubdivision();
+        }
+    };
+    public SubDivision.MapBorderMode getMapBorderMode() { return mapBorderMode.get(); }
+    public SimpleObjectProperty<SubDivision.MapBorderMode> mapBorderModeProperty() { return mapBorderMode; }
+    public void setMapBorderMode(SubDivision.MapBorderMode mapBorderMode) { this.mapBorderMode.set(mapBorderMode); }
 
     // =========================================================================
     // CONSTRUCTORS
@@ -120,13 +160,13 @@
     // PRIVATE METHODS
 
     private void updateSubdivision() {
-        final int iterations = subdivision.get();
+        final int iterations = subdivisionLevel.get();
         if (iterations == 0) {
             subdividedMesh = null;
         } else {
             subdividedMesh = getMesh();
             for (int i=0; i<iterations; i++) {
-                subdividedMesh = SubDivision.subdivide(subdividedMesh);
+                subdividedMesh = SubDivision.subdivide(subdividedMesh, boundaryMode.get(), mapBorderMode.get());
             }
         }
         updateMesh();
@@ -134,23 +174,28 @@
 
     private void updateMesh() {
         PolygonMesh pmesh = subdividedMesh != null ? subdividedMesh : getMesh();
-        if (pmesh == null || pmesh.points == null || pmesh.faces == null || pmesh.texCoords == null) {
+        if (pmesh == null || pmesh.faces == null || pmesh.texCoords == null) {
             meshView.setMesh(null);
             return;
         }
         final boolean isWireframe = getDrawMode() == DrawMode.LINE;
         if (DEBUG) System.out.println("UPDATE MESH -- "+(isWireframe?"WIREFRAME":"SOLID"));
         final TriangleMesh triangleMesh = new TriangleMesh();
-        // get triangle mesh points
-        final ObservableFloatArray points =  triangleMesh.getPoints();
-        // copy over points
-        final int numOfPoints = pmesh.points.length/TriangleMesh.NUM_COMPONENTS_PER_POINT;
+        final int numOfPoints = pmesh.getPoints().size()/TriangleMesh.NUM_COMPONENTS_PER_POINT;
         if (DEBUG) System.out.println("numOfPoints = " + numOfPoints);
-        points.addAll(pmesh.points);
+        
+        if(isWireframe) {
+            // create points and copy over points to the first part of the array
+            float [] pointsArray = new float [pmesh.getPoints().size() + pmesh.getNumEdgesInFaces()*3];
+            System.arraycopy(pmesh.getPoints().toArray(null), 0, pointsArray, 0, pmesh.getPoints().size());
+            int pointsInd = pmesh.getPoints().size();
 
-        if(isWireframe) {
             // create faces and add point for each edge
-            ObservableIntegerArray faces = triangleMesh.getFaces();
+            final int numOfFacesBefore = pmesh.faces.length;
+            final int numOfFacesAfter = pmesh.getNumEdgesInFaces();
+            int [] facesArray = new int [numOfFacesAfter * TriangleMesh.NUM_COMPONENTS_PER_FACE];
+            int facesInd = 0;
+            
             for(int[] face: pmesh.faces) {
                 if (DEBUG) System.out.println("face.length = " + (face.length/2)+"  -- "+Arrays.toString(face));
                 int lastPointIndex = face[face.length-2];
@@ -158,37 +203,47 @@
                 for (int p=0;p<face.length;p+=2) {
                     int pointIndex = face[p];
                     if (DEBUG) System.out.println("        connecting point["+lastPointIndex+"] to point[" + pointIndex+"]");
-                    faces.addAll(lastPointIndex,0,pointIndex,0,points.size()/TriangleMesh.NUM_COMPONENTS_PER_POINT,0);
-                    final int numOfPoints2 = points.size()/TriangleMesh.NUM_COMPONENTS_PER_POINT;
+                    facesArray[facesInd++] = lastPointIndex;
+                    facesArray[facesInd++] = 0;
+                    facesArray[facesInd++] = pointIndex;
+                    facesArray[facesInd++] = 0;
+                    facesArray[facesInd++] = pointsInd/TriangleMesh.NUM_COMPONENTS_PER_POINT;
+                    facesArray[facesInd++] = 0;
+                    final int numOfPoints2 = pointsInd/TriangleMesh.NUM_COMPONENTS_PER_POINT;
                     if (DEBUG) System.out.println("            numOfPoints = " + numOfPoints2);
                     // get start and end point
-                    final float x1 = points.get(lastPointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT);
-                    final float y1 = points.get((lastPointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+1);
-                    final float z1 = points.get((lastPointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+2);
-                    final float x2 = points.get(pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT);
-                    final float y2 = points.get((pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+1);
-                    final float z2 = points.get((pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+2);
+                    final float x1 = pointsArray[lastPointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT];
+                    final float y1 = pointsArray[lastPointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT+1];
+                    final float z1 = pointsArray[lastPointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT+2];
+                    final float x2 = pointsArray[pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT];
+                    final float y2 = pointsArray[pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT+1];
+                    final float z2 = pointsArray[pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT+2];
                     final float distance = Math.abs(distanceBetweenPoints(x1,y1,z1,x2,y2,z2));
                     final float offset = distance/1000;
                     // add new point
-                    points.addAll(
-                            points.get(pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+offset,
-                            points.get((pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+1)+offset,
-                            points.get((pointIndex*TriangleMesh.NUM_COMPONENTS_PER_POINT)+2)+offset);
-                    if (DEBUG) System.out.println("            faces.size() = " + faces.size());
+                    pointsArray[pointsInd++] = x2 + offset;
+                    pointsArray[pointsInd++] = y2 + offset;
+                    pointsArray[pointsInd++] = z2 + offset;
+                    if (DEBUG) System.out.println("            facesInd = " + facesInd);
                     lastPointIndex = pointIndex;
                 }
             }
-            final int numOfFacesBefore = pmesh.faces.length;
+            triangleMesh.getPoints().addAll(pointsArray);
+            triangleMesh.getFaces().addAll(facesArray);
             if (DEBUG) System.out.println("numOfFacesBefore = " + numOfFacesBefore);
-            final int numOfFacesAfter = faces.size()/TriangleMesh.NUM_COMPONENTS_PER_FACE;
             if (DEBUG) System.out.println("numOfFacesAfter = " + numOfFacesAfter);
-
             // set simple texCoords for wireframe
             triangleMesh.getTexCoords().addAll(0,0);
         } else {
+            // copy over points
+            triangleMesh.getPoints().addAll(pmesh.getPoints());
+        
             // create faces and break into triangles
-            ObservableIntegerArray faces = triangleMesh.getFaces();
+            final int numOfFacesBefore = pmesh.faces.length;
+            final int numOfFacesAfter = pmesh.getNumEdgesInFaces() - 2*numOfFacesBefore;
+            int [] facesArray = new int [numOfFacesAfter * TriangleMesh.NUM_COMPONENTS_PER_FACE];
+            int facesInd = 0;
+            
             for(int[] face: pmesh.faces) {
                 if (DEBUG) System.out.println("face.length = " + face.length+"  -- "+Arrays.toString(face));
                 int firstPointIndex = face[0];
@@ -198,21 +253,18 @@
                 for (int p=4;p<face.length;p+=2) {
                     int pointIndex = face[p];
                     int texIndex = face[p+1];
-                    faces.addAll(
-                            firstPointIndex,
-                            firstTexIndex,
-                            lastPointIndex,
-                            lastTexIndex,
-                            pointIndex,
-                            texIndex);
-//                    if (DEBUG) System.out.println("        faces.size() = " + faces.size());
+                    facesArray[facesInd++] = firstPointIndex;
+                    facesArray[facesInd++] = firstTexIndex;
+                    facesArray[facesInd++] = lastPointIndex;
+                    facesArray[facesInd++] = lastTexIndex;
+                    facesArray[facesInd++] = pointIndex;
+                    facesArray[facesInd++] = texIndex;
                     lastPointIndex = pointIndex;
                     lastTexIndex = texIndex;
                 }
             }
-            final int numOfFacesBefore = pmesh.faces.length;
+            triangleMesh.getFaces().addAll(facesArray);
             if (DEBUG) System.out.println("numOfFacesBefore = " + numOfFacesBefore);
-            final int numOfFacesAfter = faces.size()/TriangleMesh.NUM_COMPONENTS_PER_FACE;
             if (DEBUG) System.out.println("numOfFacesAfter = " + numOfFacesAfter);
             // copy over texCoords
             triangleMesh.getTexCoords().addAll(pmesh.texCoords);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/SkinningMesh.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,108 @@
+
+package com.javafx.experiments.shape3d;
+
+import com.javafx.experiments.importers.maya.Joint;
+import java.util.ArrayList;
+import java.util.List;
+import javafx.collections.ObservableFloatArray;
+import javafx.geometry.Point3D;
+import javafx.scene.shape.TriangleMesh;
+import javafx.scene.transform.Affine;
+import javafx.scene.transform.NonInvertibleTransformException;
+import javafx.scene.transform.Transform;
+
+/**
+ * PolygonMesh that updates itself when the joint transforms are updated.
+ * Assumes that the dimensions of weights is nJoints x nPoints
+ */
+public class SkinningMesh extends PolygonMesh {
+    private final Point3D[][] relativePoints; // nPoints x nJoints
+    private final float[][] weights; // nPoints x nJoints
+    private final List<Integer>[] weightIndices;
+    private final List<Joint> joints;
+    private final int nPoints;
+    private final int nJoints;
+    private Transform meshInverseTransform;
+
+    public SkinningMesh(PolygonMesh mesh, Transform meshTransform, float[][] weights, Affine[] bindTransforms, List<Joint> joints) {
+        this.getPoints().addAll(mesh.getPoints());
+        this.texCoords = mesh.texCoords;
+        this.faces = mesh.faces;
+        
+        this.weights = weights;
+        this.joints = joints;
+        
+        nJoints = joints.size();
+        nPoints = getPoints().size()/ TriangleMesh.NUM_COMPONENTS_PER_POINT;
+        
+        try {
+            meshInverseTransform = meshTransform.createInverse();
+        } catch (NonInvertibleTransformException ex) {
+            System.err.println("Caught NonInvertibleTransformException: " + ex.getMessage());
+        }
+        
+        weightIndices = new List[nPoints];
+        for (int i = 0; i < nPoints; i++) {
+            weightIndices[i] = new ArrayList<Integer>();
+            for (int j = 0; j < nJoints; j++) {
+                if (weights[i][j] != 0.0f) {
+                    weightIndices[i].add(new Integer(j));
+                }
+            }
+        }
+        
+        ObservableFloatArray points = getPoints();
+        relativePoints = new Point3D[nPoints][nJoints];
+        for (int j = 0; j < nJoints; j++) {
+            Transform postBindTransform = bindTransforms[j].createConcatenation(meshTransform);
+            for (int i = 0; i < nPoints; i++) {
+                relativePoints[i][j] = postBindTransform.transform(points.get(3*i), points.get(3*i+1), points.get(3*i+2));
+            }
+        }
+    }
+    
+    public void update() {
+        Transform[] preJointTransforms = new Transform[nJoints];
+        for (int j = 0; j < nJoints; j++) {
+            preJointTransforms[j] = meshInverseTransform.createConcatenation(joints.get(j).getLocalToSceneTransform());
+        }
+
+        float[] points = new float [getPoints().size()];
+        
+        for (int i = 0; i < nPoints; i++) {
+            if (!weightIndices[i].isEmpty()) {
+                Point3D weightedPoint = new Point3D(0,0,0);
+                for (Integer j : weightIndices[i]) {
+                    Point3D absolutePoint = preJointTransforms[j].transform(relativePoints[i][j]);
+                    weightedPoint = weightedPoint.add(absolutePoint.multiply(weights[i][j]));
+                }
+                points[3*i] = (float) weightedPoint.getX();
+                points[3*i+1] = (float) weightedPoint.getY();
+                points[3*i+2] = (float) weightedPoint.getZ();
+            }
+        }
+        getPoints().set(0, points, 0, points.length);
+        
+//        // The following loop is equivalent to the one above, the difference
+//        // being that this one is more straight-forward (it checks and skips
+//        // the zero weights).
+//        for (int i = 0; i < nPoints; i++) {
+//            Point3D weightedPoint = new Point3D(0,0,0);
+//            boolean isVertexInfluenced = false;
+//            for (int j = 0; j < nJoints; j++) {
+//                if (weights[i][j] != 0.0f) {
+//                    isVertexInfluenced = true;
+//                    Point3D absolutePoint = preJointTransforms[j].transform(relativePoints[i][j]);
+//                    weightedPoint = weightedPoint.add(absolutePoint.multiply(weights[i][j]));
+//                }
+//            }
+//            if (isVertexInfluenced) {
+//                points[3*i] = (float) weightedPoint.getX();
+//                points[3*i+1] = (float) weightedPoint.getY();
+//                points[3*i+2] = (float) weightedPoint.getZ();
+//            }
+//        }
+//        getPoints().set(0, points, 0, points.length);
+        
+    }
+}
--- a/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/SubDivision.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/shape3d/SubDivision.java	Wed Jun 19 13:57:16 2013 -0700
@@ -6,12 +6,13 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import javafx.geometry.Point2D;
 import javafx.geometry.Point3D;
 
 
 /**
  * 
- * @author akouznet
+ * Catmull Clark subdivision surface
  */
 public class SubDivision {
     
@@ -20,44 +21,85 @@
     private FaceInfo[] faceInfos;
     private PointInfo[] pointInfos;
     private float[] points;
+    private float[] texCoords;
     private int[] reindex;
     private int newPointIndex;
+    private int newTexCoordIndex;
+    private BoundaryMode boundaryMode;
+    private MapBorderMode mapBorderMode;
 
-    public SubDivision(PolygonMesh oldMesh) {
+    /** 
+     * Describes whether the edges and points at the boundary are treated as creases
+     */
+    public enum BoundaryMode {
+        /**
+         * only edges at the boundary are treated as creases
+         */
+        CREASE_EDGES, 
+        /**
+         * edges and points at the boundary are treated as creases
+         */
+        CREASE_ALL
+    }
+    
+    /**
+     * describes how the new texture coordinate for the control point is defined
+     */
+    public enum MapBorderMode {
+        /**
+         * keeps the same uvs for all control points
+         */
+        NOT_SMOOTH, 
+        /**
+         * smooths uvs of points at corners
+         */
+        SMOOTH_INTERNAL, 
+        /**
+         * smooths uvs of points at boundaries (and creases [in the future when creases are defined])
+         */
+        SMOOTH_ALL
+    }
+    
+    public SubDivision(PolygonMesh oldMesh, BoundaryMode boundaryMode, MapBorderMode mapBorderMode) {
         this.oldMesh = oldMesh;
+        this.boundaryMode = boundaryMode;
+        this.mapBorderMode = mapBorderMode;
     }
     
     public PolygonMesh subdivide() {
         collectInfo();
         calcEdgePoints();
         
-        points = new float[oldMesh.points.length + faceInfos.length * 3 + edgeInfos.size() * 3];
-        List<int[]> faces = new ArrayList<>(oldMesh.faces.length * 4);
+        points = new float[oldMesh.getPoints().size() + faceInfos.length * 3 + edgeInfos.size() * 3];
+        texCoords = new float[oldMesh.getNumEdgesInFaces() * 4 * 2];
+        int[][] faces = new int[oldMesh.getNumEdgesInFaces()][8];
         newPointIndex = 0;
-        reindex = new int[oldMesh.points.length]; // indexes incremented by 1, 0 reserved for empty
+        newTexCoordIndex = 0;
+        reindex = new int[oldMesh.getPoints().size()]; // indexes incremented by 1, 0 reserved for empty
         
+        int newFacesInd = 0;
         for (int f = 0; f < oldMesh.faces.length; f++) {
             FaceInfo faceInfo = faceInfos[f];
             int[] oldFaces = oldMesh.faces[f];
             for (int p = 0; p < oldFaces.length; p += 2) {
-                int p0 = getPointNewIndex(oldFaces[p]);
-                int t0 = oldFaces[p + 1];
-                int p1 = getPointNewIndex(faceInfo.edges[(p / 2 + 1) % faceInfo.edges.length]);
-                int t1 = t0; // TODO implement texture coordinate subdivision
-                int p2 = getPointNewIndex(faceInfo);
-                int t2 = t0; // TODO implement texture coordinate subdivision
-                int p3 = getPointNewIndex(faceInfo.edges[p / 2]);
-                int t3 = t0; // TODO implement texture coordinate subdivision
-                faces.add(new int[] { p0, t0, p1, t1, p2, t2, p3, t3 } );
+                faces[newFacesInd][0] = getPointNewIndex(oldFaces[p]);
+                faces[newFacesInd][1] = getTexCoordNewIndex(faceInfo, oldFaces[p], oldFaces[p+1]);
+                faces[newFacesInd][2] = getPointNewIndex(faceInfo, (p / 2 + 1) % faceInfo.edges.length);
+                faces[newFacesInd][3] = getTexCoordNewIndex(faceInfo, (p / 2 + 1) % faceInfo.edges.length);
+                faces[newFacesInd][4] = getPointNewIndex(faceInfo);
+                faces[newFacesInd][5] = getTexCoordNewIndex(faceInfo);
+                faces[newFacesInd][6] = getPointNewIndex(faceInfo, p / 2);
+                faces[newFacesInd][7] = getTexCoordNewIndex(faceInfo, p / 2);
+                newFacesInd++;
             }
         }
-        
-        PolygonMesh newMesh = new PolygonMesh(points, oldMesh.texCoords, faces.toArray(new int[0][]));
+        PolygonMesh newMesh = new PolygonMesh(points, texCoords, faces);
         return newMesh;
     }
     
-    public static PolygonMesh subdivide(PolygonMesh oldMesh) {
-        return new SubDivision(oldMesh).subdivide();
+    public static PolygonMesh subdivide(PolygonMesh oldMesh, BoundaryMode boundaryMode, MapBorderMode mapBorderMode) {
+        SubDivision subDivision = new SubDivision(oldMesh, boundaryMode, mapBorderMode);
+        return subDivision.subdivide();
     }
 
     private void addEdge(Edge edge, FaceInfo faceInfo, Point3D midPoint) {
@@ -79,11 +121,20 @@
         pointInfo.edges.add(edge);
         pointInfo.faces.add(faceInfo);
     }
+    
+    private void addPoint(int point, Edge edge) {
+        PointInfo pointInfo = pointInfos[point];
+        if (pointInfo == null) {
+            pointInfo = new PointInfo();
+            pointInfos[point] = pointInfo;
+        }
+        pointInfo.edges.add(edge);
+    }
 
     private void collectInfo() {
         edgeInfos = new HashMap<>(oldMesh.faces.length * 2);
         faceInfos = new FaceInfo[oldMesh.faces.length];
-        pointInfos = new PointInfo[oldMesh.points.length / 3];
+        pointInfos = new PointInfo[oldMesh.getPoints().size() / 3];
         
         for (int f = 0; f < oldMesh.faces.length; f++) {
             int[] face = oldMesh.faces[f];
@@ -93,107 +144,186 @@
             if (n < 3) {
                 continue;
             }
-            int from = face[n * 2 - 2];
-            double fx, fy, fz;
-            double tx, ty, tz;
-            double x = 0, y = 0, z = 0;
-            fx = oldMesh.points[from * 3];
-            fy = oldMesh.points[from * 3 + 1];
-            fz = oldMesh.points[from * 3 + 2];
-            for (int i = 0; i < n * 2; i += 2) {
-                int to = face[i];
-                tx = oldMesh.points[to * 3];
-                ty = oldMesh.points[to * 3 + 1];
-                tz = oldMesh.points[to * 3 + 2];
+            int from = face[(n-1) * 2];
+            int texFrom = face[(n-1) * 2 + 1];
+            double fx, fy, fz, fu, fv;
+            double tx, ty, tz, tu, tv;
+            double x = 0, y = 0, z = 0, u = 0, v = 0;
+            fx = oldMesh.getPoints().get(from * 3);
+            fy = oldMesh.getPoints().get(from * 3 + 1);
+            fz = oldMesh.getPoints().get(from * 3 + 2);
+            fu = oldMesh.texCoords[texFrom * 2];
+            fv = oldMesh.texCoords[texFrom * 2 + 1];
+            for (int i = 0; i < n; i++) {
+                int to = face[i * 2];
+                int texTo = face[i * 2 + 1];
+                tx = oldMesh.getPoints().get(to * 3);
+                ty = oldMesh.getPoints().get(to * 3 + 1);
+                tz = oldMesh.getPoints().get(to * 3 + 2);
+                tu = oldMesh.texCoords[texTo * 2];
+                tv = oldMesh.texCoords[texTo * 2 + 1];
                 Point3D midPoint = new Point3D((fx + tx) / 2, (fy + ty) / 2, (fz + tz) / 2);
+                Point2D midTexCoord = new Point2D((fu + tu) / 2, (fv + tv) / 2);
                 Edge edge = new Edge(from, to);
-                faceInfo.edges[i / 2] = edge;
+                faceInfo.edges[i] = edge;
+                faceInfo.edgeTexCoords[i] = midTexCoord;
                 addEdge(edge, faceInfo, midPoint);
                 addPoint(to, faceInfo, edge);
-                fx = tx; fy = ty; fz = tz;
-                x += tx / n; y += ty / n; z += tz / n;
+                addPoint(from, edge);
+                fx = tx; fy = ty; fz = tz; fu = tu; fv = tv;
+                x += tx / n; y += ty / n; z += tz / n; u += tu / n; v += tv / n;
                 from = to;
+                texFrom = texTo;
             }
             faceInfo.point = new Point3D(x, y, z);
+            faceInfo.texCoord = new Point2D(u, v);
         }
     }
 
     private void calcEdgePoints() {
         for (EdgeInfo edgeInfo : edgeInfos.values()) {
-            int n = edgeInfo.faces.size() + 2;
-            double x = edgeInfo.midPoint.getX() * 2 / n;
-            double y = edgeInfo.midPoint.getY() * 2 / n;
-            double z = edgeInfo.midPoint.getZ() * 2 / n;
-            for (FaceInfo faceInfo : edgeInfo.faces) {
-                Point3D facePoint = faceInfo.point;
-                x += facePoint.getX() / n;
-                y += facePoint.getY() / n;
-                z += facePoint.getZ() / n;
+            if (edgeInfo.isBoundary()) {
+                edgeInfo.edgePoint = edgeInfo.midPoint;
+            } else {
+                int n = edgeInfo.faces.size() + 2;
+                double x = edgeInfo.midPoint.getX() * 2 / n;
+                double y = edgeInfo.midPoint.getY() * 2 / n;
+                double z = edgeInfo.midPoint.getZ() * 2 / n;
+                for (FaceInfo faceInfo : edgeInfo.faces) {
+                    Point3D facePoint = faceInfo.point;
+                    x += facePoint.getX() / n;
+                    y += facePoint.getY() / n;
+                    z += facePoint.getZ() / n;
+                }
+                edgeInfo.edgePoint = new Point3D(x, y, z);
             }
-            edgeInfo.edgePoint = new Point3D(x, y, z);
         }
     }
 
-    private void calcControlPoint(int srcIndex, int destIndex) {
+    private void calcControlPoint(int srcPointIndex, int destPointIndex) {
+        PointInfo pointInfo = pointInfos[srcPointIndex];
         double x, y, z;
-        PointInfo pointInfo = pointInfos[srcIndex];
-        int n = pointInfo.faces.size();
-        x = oldMesh.points[srcIndex * 3] * (n - 3.0) / n;
-        y = oldMesh.points[srcIndex * 3 + 1] * (n - 3.0) / n;
-        z = oldMesh.points[srcIndex * 3 + 2] * (n - 3.0) / n;
-        for (FaceInfo faceInfo : pointInfo.faces) {
-            Point3D point = faceInfo.point;
-            x += point.getX() / n / n;
-            y += point.getY() / n / n;
-            z += point.getZ() / n / n;
+        if (pointInfo.isBoundary()) {
+            if ((boundaryMode == BoundaryMode.CREASE_EDGES) || pointInfo.hasInternalEdge()) {
+                x = oldMesh.getPoints().get(srcPointIndex * 3) / 2;
+                y = oldMesh.getPoints().get(srcPointIndex * 3 + 1) / 2;
+                z = oldMesh.getPoints().get(srcPointIndex * 3 + 2) / 2;
+                for (Edge edge : pointInfo.edges) {
+                    EdgeInfo edgeInfo = edgeInfos.get(edge);
+                    if (edgeInfo.isBoundary()) {
+                        x += edgeInfo.edgePoint.getX() / 4;
+                        y += edgeInfo.edgePoint.getY() / 4;
+                        z += edgeInfo.edgePoint.getZ() / 4;
+                    }
+                }
+            } else {
+                x = oldMesh.getPoints().get(srcPointIndex * 3);
+                y = oldMesh.getPoints().get(srcPointIndex * 3 + 1);
+                z = oldMesh.getPoints().get(srcPointIndex * 3 + 2);
+            }
+        } else {
+            int n = pointInfo.faces.size();
+            x = oldMesh.getPoints().get(srcPointIndex * 3) * (n - 3.0) / n;
+            y = oldMesh.getPoints().get(srcPointIndex * 3 + 1) * (n - 3.0) / n;
+            z = oldMesh.getPoints().get(srcPointIndex * 3 + 2) * (n - 3.0) / n;
+            for (FaceInfo faceInfo : pointInfo.faces) {
+                Point3D point = faceInfo.point;
+                x += point.getX() / n / n;
+                y += point.getY() / n / n;
+                z += point.getZ() / n / n;
+            }
+            for (Edge edge : pointInfo.edges) {
+                EdgeInfo edgeInfo = edgeInfos.get(edge);
+                x += edgeInfo.midPoint.getX() * 2 / n / n;
+                y += edgeInfo.midPoint.getY() * 2 / n / n;
+                z += edgeInfo.midPoint.getZ() * 2 / n / n;
+            }
         }
-        for (Edge edge : pointInfo.edges) {
-            EdgeInfo edgeInfo = edgeInfos.get(edge);
-            x += edgeInfo.midPoint.getX() * 2 / n / n;
-            y += edgeInfo.midPoint.getY() * 2 / n / n;
-            z += edgeInfo.midPoint.getZ() * 2 / n / n;
-        }
-        points[destIndex * 3] = (float) x;
-        points[destIndex * 3 + 1] = (float) y;
-        points[destIndex * 3 + 2] = (float) z;
+        points[destPointIndex * 3] = (float) x;
+        points[destPointIndex * 3 + 1] = (float) y;
+        points[destPointIndex * 3 + 2] = (float) z;
     }
 
-    private int getPointNewIndex(int pointIndex) {
-        int index = reindex[pointIndex] - 1;
-        if (index == -1) {
-            index = newPointIndex;
-            reindex[pointIndex] = index + 1;
-            newPointIndex++;
-            calcControlPoint(pointIndex, index);
+    private void calcControlTexCoord(FaceInfo faceInfo, int srcPointIndex, int srcTexCoordIndex, int destTexCoordIndex){
+        PointInfo pointInfo = pointInfos[srcPointIndex];
+        if ((mapBorderMode == MapBorderMode.SMOOTH_ALL && pointInfo.isBoundary()) || 
+                (mapBorderMode == MapBorderMode.SMOOTH_INTERNAL && !pointInfo.hasInternalEdge())) {
+            double u = oldMesh.texCoords[srcTexCoordIndex * 2] / 2;
+            double v = oldMesh.texCoords[srcTexCoordIndex * 2 + 1] / 2;
+            for (int i = 0; i < faceInfo.edges.length; i++) {
+                if ((faceInfo.edges[i].to == srcPointIndex) || (faceInfo.edges[i].from == srcPointIndex)) {
+                    u += faceInfo.edgeTexCoords[i].getX() / 4;
+                    v += faceInfo.edgeTexCoords[i].getY() / 4;
+                }
+            }
+            texCoords[destTexCoordIndex * 2] = (float) u;
+            texCoords[destTexCoordIndex * 2 + 1] = (float) v;
+        } else {
+            texCoords[destTexCoordIndex * 2] = oldMesh.texCoords[srcTexCoordIndex * 2];
+            texCoords[destTexCoordIndex * 2 + 1] = oldMesh.texCoords[srcTexCoordIndex * 2 + 1];
         }
-        return index;
     }
 
-    private int getPointNewIndex(Edge edge) {
+    private int getPointNewIndex(int srcPointIndex) {
+        int destPointIndex = reindex[srcPointIndex] - 1;
+        if (destPointIndex == -1) {
+            destPointIndex = newPointIndex;
+            reindex[srcPointIndex] = destPointIndex + 1;
+            newPointIndex++;
+            calcControlPoint(srcPointIndex, destPointIndex);
+        }
+        return destPointIndex;
+    }
+    
+    private int getPointNewIndex(FaceInfo faceInfo, int edgeInd) {
+        Edge edge = faceInfo.edges[edgeInd];
         EdgeInfo edgeInfo = edgeInfos.get(edge);
-        int index = edgeInfo.newIndex - 1;
-        if (index == -1) {
-            index = newPointIndex;
-            edgeInfo.newIndex = index + 1;
+        int destPointIndex = edgeInfo.newPointIndex - 1;
+        if (destPointIndex == -1) {
+            destPointIndex = newPointIndex;
+            edgeInfo.newPointIndex = destPointIndex + 1;
             newPointIndex++;
-            points[index * 3] = (float) edgeInfo.edgePoint.getX();
-            points[index * 3 + 1] = (float) edgeInfo.edgePoint.getY();
-            points[index * 3 + 2] = (float) edgeInfo.edgePoint.getZ();
+            points[destPointIndex * 3] = (float) edgeInfo.edgePoint.getX();
+            points[destPointIndex * 3 + 1] = (float) edgeInfo.edgePoint.getY();
+            points[destPointIndex * 3 + 2] = (float) edgeInfo.edgePoint.getZ();
         }
-        return index;
+        return destPointIndex;
     }
 
     private int getPointNewIndex(FaceInfo faceInfo) {
-        int index = faceInfo.newIndex - 1;
-        if (index == -1) {
-            index = newPointIndex;
-            faceInfo.newIndex = index + 1;
+        int destPointIndex = faceInfo.newPointIndex - 1;
+        if (destPointIndex == -1) {
+            destPointIndex = newPointIndex;
+            faceInfo.newPointIndex = destPointIndex + 1;
             newPointIndex++;
-            points[index * 3] = (float) faceInfo.point.getX();
-            points[index * 3 + 1] = (float) faceInfo.point.getY();
-            points[index * 3 + 2] = (float) faceInfo.point.getZ();
+            points[destPointIndex * 3] = (float) faceInfo.point.getX();
+            points[destPointIndex * 3 + 1] = (float) faceInfo.point.getY();
+            points[destPointIndex * 3 + 2] = (float) faceInfo.point.getZ();
         }
-        return index;
+        return destPointIndex;
+    }
+
+    private int getTexCoordNewIndex(FaceInfo faceInfo, int srcPointIndex, int srcTexCoordIndex) {
+        int destTexCoordIndex = newTexCoordIndex;
+        newTexCoordIndex++;
+        calcControlTexCoord(faceInfo, srcPointIndex, srcTexCoordIndex, destTexCoordIndex);
+        return destTexCoordIndex;
+    }
+    
+    private int getTexCoordNewIndex(FaceInfo faceInfo, int edgeInd) {
+        int destTexCoordIndex = newTexCoordIndex;
+        newTexCoordIndex++;
+        texCoords[destTexCoordIndex * 2] = (float) faceInfo.edgeTexCoords[edgeInd].getX();
+        texCoords[destTexCoordIndex * 2 + 1] = (float) faceInfo.edgeTexCoords[edgeInd].getY();
+        return destTexCoordIndex;
+    }
+    
+    private int getTexCoordNewIndex(FaceInfo faceInfo) {
+        int destTexCoordIndex = newTexCoordIndex;
+        newTexCoordIndex++;
+        texCoords[destTexCoordIndex * 2] = (float) faceInfo.texCoord.getX();
+        texCoords[destTexCoordIndex * 2 + 1] = (float) faceInfo.texCoord.getY();
+        return destTexCoordIndex;
     }
 
     private static class Edge {
@@ -234,22 +364,56 @@
     private static class EdgeInfo {
         Point3D midPoint;
         Point3D edgePoint;
-        int newIndex;
+        int newPointIndex;
         List<FaceInfo> faces = new ArrayList<>(2);
+        
+        /**
+         * an edge is in the boundary if it has only one adjacent face
+         */
+        public boolean isBoundary() {
+            return faces.size() == 1;
+        }
     }
     
-    private static class PointInfo {
+    private class PointInfo {
         List<FaceInfo> faces = new ArrayList<>(4);
         Set<Edge> edges = new HashSet<>(4);
+        
+        /**
+         * A point is in the boundary if any of its adjacent edges is in the boundary
+         */
+        public boolean isBoundary() {
+            for (Edge edge : edges) {
+                EdgeInfo edgeInfo = edgeInfos.get(edge);
+                if (edgeInfo.isBoundary())
+                    return true;
+            }
+            return false;
+        }
+        
+        /**
+         * A point is internal if at least one of its adjacent edges is not in the boundary
+         */
+        public boolean hasInternalEdge() {
+            for (Edge edge : edges) {
+                EdgeInfo edgeInfo = edgeInfos.get(edge);
+                if (!edgeInfo.isBoundary())
+                    return true;
+            }
+            return false;
+        }
     }
     
     private static class FaceInfo {
         Point3D point;
-        int newIndex;
+        Point2D texCoord;
+        int newPointIndex;
         Edge[] edges;
+        Point2D[] edgeTexCoords;
 
         public FaceInfo(int n) {
             edges = new Edge[n];
+            edgeTexCoords = new Point2D[n];
         }
     }
 }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/test/java/com/javafx/experiments/exporters/javasource/JavaSourceExporterTestApp.java	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,37 @@
+package com.javafx.experiments.exporters.javasource;
+
+import java.io.File;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.stage.Stage;
+import com.javafx.experiments.importers.Optimizer;
+import com.javafx.experiments.importers.maya.MayaImporter;
+
+/**
+ * Simple Test application to load 3D file and export as Java Class
+ */
+public class JavaSourceExporterTestApp extends Application {
+    @Override public void start(Stage primaryStage) throws Exception {
+        String URL = "file:///Users/jpotts/Projects/jfx-bluray-8.0/apps/bluray/BluRay/src/bluray/botmenu/dukeBot.ma";
+
+        MayaImporter importer = new MayaImporter();
+        importer.load(URL);
+
+        Optimizer optimizer = new Optimizer(importer.getTimeline(),importer.getRoot());
+        optimizer.optimize();
+
+        File out = new File("/Users/jpotts/Projects/jfx-bluray-8.0/apps/bluray/BluRay/src/bluray/botmenu/GreenBot.java");
+        JavaSourceExporter javaSourceExporter = new JavaSourceExporter(
+                URL.substring(0,URL.lastIndexOf('/')),
+                importer.getRoot(), importer.getTimeline(),
+                "bluray.botmenu",
+                out);
+        javaSourceExporter.export();
+
+        Platform.exit();
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
--- a/apps/experiments/Modena/Modena.iml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/Modena/Modena.iml	Wed Jun 19 13:57:16 2013 -0700
@@ -53,9 +53,12 @@
     <orderEntry type="module" module-name="prism-ps" />
     <orderEntry type="module" module-name="prism-util" />
     <orderEntry type="module" module-name="test-stub-toolkit" />
-    <orderEntry type="module" module-name="webkit" />
+    <orderEntry type="module" module-name="webview" />
     <orderEntry type="inheritedJdk" />
     <orderEntry type="library" name="webkit-impl" level="project" />
+    <orderEntry type="module" module-name="javafx-builders" />
+    <orderEntry type="module" module-name="javafx-font-t2k" />
+    <orderEntry type="module" module-name="prism-sw" />
   </component>
 </module>
 
--- a/apps/experiments/Modena/build.xml	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/Modena/build.xml	Wed Jun 19 13:57:16 2013 -0700
@@ -9,6 +9,7 @@
 <!-- in the project's Project Properties dialog box.-->
 <project name="Modena" default="default" basedir=".">
     <description>Builds, tests, and runs the project Modena.</description>
+    <import file="../../build-tasks.xml"/>
     <import file="nbproject/build-impl.xml"/>
     <!--
 
@@ -51,8 +52,7 @@
       -init-macrodef-junit:     defines macro for junit execution
       -init-macrodef-debug:     defines macro for class debugging
       -init-macrodef-java:      defines macro for class execution
-      -do-jar-with-manifest:    JAR building (if you are using a manifest)
-      -do-jar-without-manifest: JAR building (if you are not using a manifest)
+      -do-jar:                  JAR building
       run:                      execution of project 
       -javadoc-build:           Javadoc generation
       test-report:              JUnit report generation
--- a/apps/experiments/Modena/nbproject/project.properties	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/Modena/nbproject/project.properties	Wed Jun 19 13:57:16 2013 -0700
@@ -37,8 +37,8 @@
 javac.deprecation=false
 javac.processorpath=\
     ${javac.classpath}
-javac.source=1.7
-javac.target=1.7
+javac.source=1.8
+javac.target=1.8
 javac.test.classpath=\
     ${javac.classpath}:\
     ${build.classes.dir}
--- a/apps/experiments/Modena/src/modena/Modena.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/Modena/src/modena/Modena.java	Wed Jun 19 13:57:16 2013 -0700
@@ -130,7 +130,7 @@
         }
     }
     
-    private final BorderPane outerRoot = new BorderPane();
+    private BorderPane outerRoot;
     private BorderPane root;
     private SamplePageNavigation samplePageNavigation;
     private SamplePage samplePage;
@@ -206,6 +206,7 @@
         // set user agent stylesheet
         updateUserAgentStyleSheet(true);
         // build Menu Bar
+        outerRoot = new BorderPane();
         outerRoot.setTop(buildMenuBar());
         outerRoot.setCenter(root);
         // build UI
--- a/apps/experiments/Modena/src/modena/SamplePage.java	Mon Jun 17 18:00:06 2013 -0700
+++ b/apps/experiments/Modena/src/modena/SamplePage.java	Wed Jun 19 13:57:16 2013 -0700
@@ -79,7 +79,7 @@
 import javafx.scene.control.ToggleGroup;
 import javafx.scene.control.Tooltip;
 import javafx.scene.control.TooltipBuilder;
-import javafx.scene.control.TreeTableViewBuilder;
+import javafx.scene.control.TreeTableView;
 import javafx.scene.control.TreeViewBuilder;
 import javafx.scene.layout.GridPane;
 import javafx.scene.layout.HBox;
@@ -88,13 +88,13 @@
 import javafx.scene.layout.VBox;
 import javafx.scene.layout.VBoxBuilder;
 import javafx.scene.paint.Color;
-import javafx.scene.web.HTMLEditorBuilder;
+import javafx.scene.web.HTMLEditor;
 
 import static modena.SamplePageChartHelper.*;
 import static modena.SamplePageHelpers.*;
 import static modena.SamplePageTableHelper.*;
-import static modena.SamplePageTreeHelper.*;
-import static modena.SamplePageTreeTableHelper.*;
+import static modena.SamplePageTreeHelper.createTreeView;
+import static modena.SamplePageTreeTableHelper.createTreeTableView;
 
 /**
  * Page showing every control in every state.
@@ -302,32 +302,32 @@
         choiceBoxLongList.add(100, "Long List");
         newSection(
                 "ChoiceBox:",
-                ChoiceBoxBuilder.create(String.class).items(sampleItems()).value("Item A").build(),
-                ChoiceBoxBuilder.create(String.class).items(choiceBoxLongList).value("Long List").build(),
-                withState(ChoiceBoxBuilder.create(String.class).items(sampleItems()).value("Item B").build(), "hover"),
-                withState(ChoiceBoxBuilder.create(String.class).items(sampleItems()).value("Item B").build(), "showing"),
-                withState(ChoiceBoxBuilder.create(String.class).items(sampleItems()).value("Item B").build(), "focused"),
-                ChoiceBoxBuilder.create(String.class).items(sampleItems()).value("Item C").disable(true).build()
+                ChoiceBoxBuilder.<String>create().items(sampleItems()).value("Item A").build(),
+                ChoiceBoxBuilder.<String>create().items(choiceBoxLongList).value("Long List").build(),
+                withState(ChoiceBoxBuilder.<String>create().items(sampleItems()).value("Item B").build(), "hover"),
+                withState(ChoiceBoxBuilder.<String>create().items(sampleItems()).value("Item B").build(), "showing"),
+                withState(ChoiceBoxBuilder.<String>create().items(sampleItems()).value("Item B").build(), "focused"),
+                ChoiceBoxBuilder.<String>create().items(sampleItems()).value("Item C").disable(true).build()
         );
         newSection(
                 "ComboBox:",
-                ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item A").build(),
-                ComboBoxBuilder.create(String.class).items(choiceBoxLongList).value("Long List").build(),
-                withState(ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item B").build(), "hover"),
-                withState(ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item B").build(), "showing"),
-                withState(ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item B").build(), "focused"),
-                ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item C").disable(true).build()
+                ComboBoxBuilder.<String>create().items(sampleItems()).value("Item A").build(),
+                ComboBoxBuilder.<String>create().items(choiceBoxLongList).value("Long List").build(),
+                withState(ComboBoxBuilder.<String>create().items(sampleItems()).value("Item B").build(), "hover"),
+                withState(ComboBoxBuilder.<String>create().items(sampleItems()).value("Item B").build(), "showing"),
+                withState(ComboBoxBuilder.<String>create().items(sampleItems()).value("Item B").build(), "focused"),
+                ComboBoxBuilder.<String>create().items(sampleItems()).value("Item C").disable(true).build()
                 );
         newSection(
                 "ComboBox\nEditable:",
-                ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item A").editable(true).build(),
-                withState(ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item B").editable(true).build(), "editable", ".arrow-button", "hover"),
-                withState(ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item B").editable(true).build(), "editable", ".arrow-button", "pressed")
+                ComboBoxBuilder.<String>create().items(sampleItems()).value("Item A").editable(true).build(),
+                withState(ComboBoxBuilder.<String>create().items(sampleItems()).value("Item B").editable(true).build(), "editable", ".arrow-button", "hover"),
+                withState(ComboBoxBuilder.<String>create().items(sampleItems()).value("Item B").editable(true).build(), "editable", ".arrow-button", "pressed")
                 );
         newSection(
                 "ComboBox\nEditable\n(More):",
-                withState(ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item B").editable(true).build(), "editable,contains-focus", ".text-field", "focused"),
-                ComboBoxBuilder.create(String.class).items(sampleItems()).value("Item C").editable(true).disable(true).build()
+                withState(ComboBoxBuilder.<String>create().items(sampleItems()).value("Item B").editable(true).build(), "editable,contains-focus", ".text-field", "focused"),
+                ComboBoxBuilder.<String>create().items(sampleItems()).value("Item C").editable(true).disable(true).build()
                 );
         newSection(
                 "Color Picker:",
@@ -465,11 +465,16 @@
         );
         newSection(
                 "HTMLEditor:",
-                HTMLEditorBuilder.create().htmlText("Hello <b>Bold</b> Text").prefWidth(650).prefHeight(120).build()
-                );
+                new HTMLEditor() {{
+                    setHtmlText("Hello <b>Bold</b> Text");
+                    setPrefSize(650, 120);
+                }});
         newSection(
                 "HTMLEditor\nFocused:",
-                withState(HTMLEditorBuilder.create().htmlText("<i>Focused</i>").prefWidth(650).prefHeight(120).build(), "focused")
+                withState(new HTMLEditor() {{
+                    setHtmlText("<i>Focused</i>");
+                    setPrefSize(650, 120);
+                }}, "focused")
                 );
         newDetailedSection(
                 new String[] { "ToolBar (H|TOP):", "normal", "overflow", "disabled" },
@@ -640,10 +645,12 @@
                 );
         newDetailedSection(
                 new String[] {"Empty:", "ListView", "TableView", "TreeView", "TreeTableView"},
-                ListViewBuilder.create(String.class).prefWidth(150).prefHeight(100).build(),
-                TableViewBuilder.create(Object.class).prefWidth(150).prefHeight(100).build(),
-                TreeViewBuilder.create(Object.class).prefWidth(150).prefHeight(100).build(),
-                TreeTableViewBuilder.create(Object.class).prefWidth(150).prefHeight(100).build()
+                ListViewBuilder.<String>create().prefWidth(150).prefHeight(100).build(),
+                TableViewBuilder.create().prefWidth(150).prefHeight(100).build(),
+                TreeViewBuilder.create().prefWidth(150).prefHeight(100).build(),
+                new TreeTableView() {{
+                    setPrefSize(150, 100);
+                }}
                 );
         newDetailedSection(
                 new String[] {"ToolTip:","inline","inline + graphic", "popup"},
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/build.xml	Wed Jun 19 13:57:16 2013 -0700
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="JavaFX apps - internal" default="default" basedir=".">
+
+    <import file="../build-defs.xml"/>
+
+    <target name="init">
+        <echo message="Building selected apps in experiments"/>
+    </target>
+
+    <target name="jar-apps" depends="init">
+        <!-- Build applications -->
+        <ant dir="3DViewer" target="jar" inheritAll="false"/>
+        <ant dir="Modena" target="jar" inheritAll="false"/>
+    </target>