BowlerKernel
Optimizer.java
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2013, 2014, Oracle and/or its affiliates.
3  * All rights reserved. Use is subject to license terms.
4  *
5  * This file is available and licensed under the following license:
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * - Redistributions of source code must retain the above copyright
12  * notice, this list of conditions and the following disclaimer.
13  * - Redistributions in binary form must reproduce the above copyright
14  * notice, this list of conditions and the following disclaimer in
15  * the documentation and/or other materials provided with the distribution.
16  * - Neither the name of Oracle Corporation nor the names of its
17  * contributors may be used to endorse or promote products derived
18  * from this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package eu.mihosoft.vrl.v3d.ext.openjfx.importers;
33 
34 import java.util.ArrayList;
35 import java.util.Comparator;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.Iterator;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 import javafx.animation.Interpolator;
43 import javafx.animation.KeyFrame;
44 import javafx.animation.KeyValue;
45 import javafx.animation.Timeline;
46 import javafx.beans.property.Property;
47 import javafx.beans.value.WritableValue;
48 import javafx.collections.FXCollections;
49 import javafx.collections.ObservableFloatArray;
50 import javafx.collections.ObservableIntegerArray;
51 import javafx.collections.ObservableList;
52 import javafx.collections.transformation.SortedList;
53 import javafx.geometry.Point2D;
54 import javafx.geometry.Point3D;
55 import javafx.scene.Group;
56 import javafx.scene.Node;
57 import javafx.scene.Parent;
58 import javafx.scene.shape.MeshView;
59 import javafx.scene.shape.TriangleMesh;
60 import javafx.scene.transform.Transform;
61 import javafx.util.Duration;
62 
63 // TODO: Auto-generated Javadoc
68 public class Optimizer {
69 
71  private Timeline timeline;
72 
74  private Node root;
75 
77  private Set<Transform> bound = new HashSet<>();
78 
80  private List<Parent> emptyParents = new ArrayList<>();
81 
83  private List<MeshView> meshViews = new ArrayList<>();
84 
86  private boolean convertToDiscrete = true;
87 
94  public Optimizer(Timeline timeline, Node root) {
95  this(timeline, root, false);
96  }
97 
105  public Optimizer(Timeline timeline, Node root, boolean convertToDiscrete) {
106  this.timeline = timeline;
107  this.root = root;
108  this.convertToDiscrete = convertToDiscrete;
109  }
110 
112  private int trRemoved, trTotal, groupsTotal, trCandidate, trEmpty;
113 
117  public void optimize() {
118  trRemoved = 0;
119  trTotal = 0;
120  trCandidate = 0;
121  trEmpty = 0;
122  groupsTotal = 0;
123  emptyParents.clear();
124 
125  parseTimeline();
126  optimize(root);
128  optimizeMeshes();
129 
130  System.out.printf("removed %d (%.2f%%) out of total %d transforms\n", trRemoved, 100d * trRemoved / trTotal, trTotal);
131  System.out.printf("there are %d more multiplications that can be done of matrices that never change\n", trCandidate);
132  System.out.printf("there are %d (%.2f%%) out of total %d groups with no transforms in them\n", trEmpty, 100d * trEmpty / groupsTotal, groupsTotal);
133  }
134 
140  private void optimize(Node node) {
141  ObservableList<Transform> transforms = node.getTransforms();
142  Iterator<Transform> iterator = transforms.iterator();
143  boolean prevIsStatic = false;
144  while (iterator.hasNext()) {
145  Transform transform = iterator.next();
146  trTotal++;
147  if (transform.isIdentity()) {
148  if (timeline == null || !bound.contains(transform)) {
149  iterator.remove();
150  trRemoved++;
151  }
152  } else {
153  if (timeline == null || !bound.contains(transform)) {
154  if (prevIsStatic) {
155  trCandidate++;
156  }
157  prevIsStatic = true;
158  } else {
159  prevIsStatic = false;
160  }
161  }
162  }
163  if (node instanceof Parent) {
164  groupsTotal++;
165  Parent p = (Parent) node;
166  for (Node n : p.getChildrenUnmodifiable()) {
167  optimize(n);
168  }
169  if (transforms.isEmpty()) {
170  Parent parent = p.getParent();
171  if (parent instanceof Group) {
172  trEmpty++;
173 // System.out.println("Empty group = " + node.getId());
174  emptyParents.add(p);
175  } else {
176 // System.err.println("parent is not group = " + parent);
177  }
178  }
179  }
180  if (node instanceof MeshView) {
181  meshViews.add((MeshView) node);
182  }
183  }
184 
188  private void optimizeMeshes() {
189  optimizePoints();
191  optimizeFaces();
192  }
193 
197  private void optimizeFaces() {
198  int total = 0, sameIndexes = 0, samePoints = 0, smallArea = 0;
199  ObservableIntegerArray newFaces = FXCollections.observableIntegerArray();
200  ObservableIntegerArray newFaceSmoothingGroups = FXCollections.observableIntegerArray();
201  for (MeshView meshView : meshViews) {
202  TriangleMesh mesh = (TriangleMesh) meshView.getMesh();
203  ObservableIntegerArray faces = mesh.getFaces();
204  ObservableIntegerArray faceSmoothingGroups = mesh.getFaceSmoothingGroups();
205  ObservableFloatArray points = mesh.getPoints();
206  newFaces.clear();
207  newFaces.ensureCapacity(faces.size());
208  newFaceSmoothingGroups.clear();
209  newFaceSmoothingGroups.ensureCapacity(faceSmoothingGroups.size());
210  int pointElementSize = mesh.getPointElementSize();
211  int faceElementSize = mesh.getFaceElementSize();
212  for (int i = 0; i < faces.size(); i += faceElementSize) {
213  total++;
214  int i1 = faces.get(i) * pointElementSize;
215  int i2 = faces.get(i + 2) * pointElementSize;
216  int i3 = faces.get(i + 4) * pointElementSize;
217  if (i1 == i2 || i1 == i3 || i2 == i3) {
218  sameIndexes++;
219  continue;
220  }
221  Point3D p1 = new Point3D(points.get(i1), points.get(i1 + 1), points.get(i1 + 2));
222  Point3D p2 = new Point3D(points.get(i2), points.get(i2 + 1), points.get(i2 + 2));
223  Point3D p3 = new Point3D(points.get(i3), points.get(i3 + 1), points.get(i3 + 2));
224  if (p1.equals(p2) || p1.equals(p3) || p2.equals(p3)) {
225  samePoints++;
226  continue;
227  }
228  double a = p1.distance(p2);
229  double b = p2.distance(p3);
230  double c = p3.distance(p1);
231  double p = (a + b + c) / 2;
232  double sqarea = p * (p - a) * (p - b) * (p - c);
233 
234  final float DEAD_FACE = 1.f/1024/1024/1024/1024; // taken from MeshNormal code
235 
236  if (sqarea < DEAD_FACE) {
237  smallArea++;
238 // System.out.printf("a = %e, b = %e, c = %e, sqarea = %e\n"
239 // + "p1 = %s\np2 = %s\np3 = %s\n", a, b, c, sqarea, p1.toString(), p2.toString(), p3.toString());
240  continue;
241  }
242  newFaces.addAll(faces, i, faceElementSize);
243  int fIndex = i / faceElementSize;
244  if (fIndex < faceSmoothingGroups.size()) {
245  newFaceSmoothingGroups.addAll(faceSmoothingGroups.get(fIndex));
246  }
247  }
248  faces.setAll(newFaces);
249  faceSmoothingGroups.setAll(newFaceSmoothingGroups);
250  faces.trimToSize();
251  faceSmoothingGroups.trimToSize();
252  }
253  int badTotal = sameIndexes + samePoints + smallArea;
254  System.out.printf("Removed %d (%.2f%%) faces with same point indexes, "
255  + "%d (%.2f%%) faces with same points, "
256  + "%d (%.2f%%) faces with small area. "
257  + "Total %d (%.2f%%) bad faces out of %d total.\n",
258  sameIndexes, 100d * sameIndexes / total,
259  samePoints, 100d * samePoints / total,
260  smallArea, 100d * smallArea / total,
261  badTotal, 100d * badTotal / total, total);
262  }
263 
267  private void optimizePoints() {
268  int total = 0, duplicates = 0, check = 0;
269 
270  Map<Point3D, Integer> pp = new HashMap<>();
271  ObservableIntegerArray reindex = FXCollections.observableIntegerArray();
272  ObservableFloatArray newPoints = FXCollections.observableFloatArray();
273 
274  for (MeshView meshView : meshViews) {
275  TriangleMesh mesh = (TriangleMesh) meshView.getMesh();
276  ObservableFloatArray points = mesh.getPoints();
277  int pointElementSize = mesh.getPointElementSize();
278  int os = points.size() / pointElementSize;
279 
280  pp.clear();
281  newPoints.clear();
282  newPoints.ensureCapacity(points.size());
283  reindex.clear();
284  reindex.resize(os);
285 
286  for (int i = 0, oi = 0, ni = 0; i < points.size(); i += pointElementSize, oi++) {
287  float x = points.get(i);
288  float y = points.get(i + 1);
289  float z = points.get(i + 2);
290  Point3D p = new Point3D(x, y, z);
291  Integer index = pp.get(p);
292  if (index == null) {
293  pp.put(p, ni);
294  reindex.set(oi, ni);
295  newPoints.addAll(x, y, z);
296  ni++;
297  } else {
298  reindex.set(oi, index);
299  }
300  }
301 
302  int ns = newPoints.size() / pointElementSize;
303 
304  int d = os - ns;
305  duplicates += d;
306  total += os;
307 
308  points.setAll(newPoints);
309  points.trimToSize();
310 
311  ObservableIntegerArray faces = mesh.getFaces();
312  for (int i = 0; i < faces.size(); i += 2) {
313  faces.set(i, reindex.get(faces.get(i)));
314  }
315 
316 // System.out.printf("There are %d (%.2f%%) duplicate points out of %d total for mesh '%s'.\n",
317 // d, 100d * d / os, os, meshView.getId());
318 
319  check += mesh.getPoints().size() / pointElementSize;
320  }
321  System.out.printf("There are %d (%.2f%%) duplicate points out of %d total.\n",
322  duplicates, 100d * duplicates / total, total);
323  System.out.printf("Now we have %d points.\n", check);
324  }
325 
329  private void optimizeTexCoords() {
330  int total = 0, duplicates = 0, check = 0;
331 
332  Map<Point2D, Integer> pp = new HashMap<>();
333  ObservableIntegerArray reindex = FXCollections.observableIntegerArray();
334  ObservableFloatArray newTexCoords = FXCollections.observableFloatArray();
335 
336  for (MeshView meshView : meshViews) {
337  TriangleMesh mesh = (TriangleMesh) meshView.getMesh();
338  ObservableFloatArray texcoords = mesh.getTexCoords();
339  int texcoordElementSize = mesh.getTexCoordElementSize();
340  int os = texcoords.size() / texcoordElementSize;
341 
342  pp.clear();
343  newTexCoords.clear();
344  newTexCoords.ensureCapacity(texcoords.size());
345  reindex.clear();
346  reindex.resize(os);
347 
348  for (int i = 0, oi = 0, ni = 0; i < texcoords.size(); i += texcoordElementSize, oi++) {
349  float x = texcoords.get(i);
350  float y = texcoords.get(i + 1);
351  Point2D p = new Point2D(x, y);
352  Integer index = pp.get(p);
353  if (index == null) {
354  pp.put(p, ni);
355  reindex.set(oi, ni);
356  newTexCoords.addAll(x, y);
357  ni++;
358  } else {
359  reindex.set(oi, index);
360  }
361  }
362 
363  int ns = newTexCoords.size() / texcoordElementSize;
364 
365  int d = os - ns;
366  duplicates += d;
367  total += os;
368 
369  texcoords.setAll(newTexCoords);
370  texcoords.trimToSize();
371 
372  ObservableIntegerArray faces = mesh.getFaces();
373  for (int i = 1; i < faces.size(); i += 2) {
374  faces.set(i, reindex.get(faces.get(i)));
375  }
376 
377 // System.out.printf("There are %d (%.2f%%) duplicate texcoords out of %d total for mesh '%s'.\n",
378 // d, 100d * d / os, os, meshView.getId());
379 
380  check += mesh.getTexCoords().size() / texcoordElementSize;
381  }
382  System.out.printf("There are %d (%.2f%%) duplicate texcoords out of %d total.\n",
383  duplicates, 100d * duplicates / total, total);
384  System.out.printf("Now we have %d texcoords.\n", check);
385  }
386 
391  ObservableList<KeyFrame> timelineKeyFrames = timeline.getKeyFrames().sorted(new KeyFrameComparator());
392 // Timeline timeline;
393  int kfTotal = timelineKeyFrames.size(), kfRemoved = 0;
394  int kvTotal = 0, kvRemoved = 0;
395  Map<Duration, KeyFrame> kfUnique = new HashMap<>();
396  Map<WritableValue, KeyValue> kvUnique = new HashMap<>();
397  MapOfLists<KeyFrame, KeyFrame> duplicates = new MapOfLists<>();
398  Iterator<KeyFrame> iterator = timelineKeyFrames.iterator();
399  while (iterator.hasNext()) {
400  KeyFrame duplicate = iterator.next();
401  KeyFrame original = kfUnique.put(duplicate.getTime(), duplicate);
402  if (original != null) {
403  kfRemoved++;
404  iterator.remove(); // removing duplicate keyFrame
405  duplicates.add(original, duplicate);
406 
407  kfUnique.put(duplicate.getTime(), original);
408  }
409  kvUnique.clear();
410  for (KeyValue kvDup : duplicate.getValues()) {
411  kvTotal++;
412  KeyValue kvOrig = kvUnique.put(kvDup.getTarget(), kvDup);
413  if (kvOrig != null) {
414  kvRemoved++;
415  if (!kvOrig.getEndValue().equals(kvDup.getEndValue()) && kvOrig.getTarget() == kvDup.getTarget()) {
416  System.err.println("KeyValues set different values for KeyFrame " + duplicate.getTime() + ":"
417  + "\n kvOrig = " + kvOrig + ", \nkvDup = " + kvDup);
418  }
419  }
420  }
421  }
422  for (KeyFrame orig : duplicates.keySet()) {
423  List<KeyValue> keyValues = new ArrayList<>();
424  for (KeyFrame dup : duplicates.get(orig)) {
425  keyValues.addAll(dup.getValues());
426  }
427  timelineKeyFrames.set(timelineKeyFrames.indexOf(orig),
428  new KeyFrame(orig.getTime(), keyValues.toArray(new KeyValue[keyValues.size()])));
429  }
430  System.out.printf("Removed %d (%.2f%%) duplicate KeyFrames out of total %d.\n",
431  kfRemoved, 100d * kfRemoved / kfTotal, kfTotal);
432  System.out.printf("Identified %d (%.2f%%) duplicate KeyValues out of total %d.\n",
433  kvRemoved, 100d * kvRemoved / kvTotal, kvTotal);
434  }
435 
439  private static class KeyInfo {
440 
442  KeyFrame keyFrame;
443 
445  KeyValue keyValue;
446 
448  boolean first;
449 
456  public KeyInfo(KeyFrame keyFrame, KeyValue keyValue) {
457  this.keyFrame = keyFrame;
458  this.keyValue = keyValue;
459  first = false;
460  }
461 
469  public KeyInfo(KeyFrame keyFrame, KeyValue keyValue, boolean first) {
470  this.keyFrame = keyFrame;
471  this.keyValue = keyValue;
472  this.first = first;
473  }
474  }
475 
482  private static class MapOfLists<K, V> extends HashMap<K, List<V>> {
483 
490  public void add(K key, V value) {
491  List<V> p = get(key);
492  if (p == null) {
493  p = new ArrayList<>();
494  put(key, p);
495  }
496  p.add(value);
497  }
498  }
499 
503  private void parseTimeline() {
504  bound.clear();
505  if (timeline == null) {
506  return;
507  }
508 // cleanUpRepeatingFramesAndValues(); // we don't need it usually as timeline is initially correct
509  SortedList<KeyFrame> sortedKeyFrames = timeline.getKeyFrames().sorted(new KeyFrameComparator());
510  MapOfLists<KeyFrame, KeyValue> toRemove = new MapOfLists<>();
511  Map<WritableValue, KeyInfo> prevValues = new HashMap<>();
512  Map<WritableValue, KeyInfo> prevPrevValues = new HashMap<>();
513  int kvTotal = 0;
514  for (KeyFrame keyFrame : sortedKeyFrames) {
515  for (KeyValue keyValue : keyFrame.getValues()) {
516  WritableValue<?> target = keyValue.getTarget();
517  KeyInfo prev = prevValues.get(target);
518  kvTotal++;
519  if (prev != null && prev.keyValue.getEndValue().equals(keyValue.getEndValue())) {
520 // if (prev != null && (prev.keyValue.equals(keyValue) || (prev.first && prev.keyValue.getEndValue().equals(keyValue.getEndValue())))) {
521  KeyInfo prevPrev = prevPrevValues.get(target);
522  if ((prevPrev != null && prevPrev.keyValue.getEndValue().equals(keyValue.getEndValue()))
523  || (prev.first && target.getValue().equals(prev.keyValue.getEndValue()))) {
524  // All prevPrev, prev and current match, so prev can be removed
525  // or prev is first and its value equals to the property existing value, so prev can be removed
526  toRemove.add(prev.keyFrame, prev.keyValue);
527  } else {
528  prevPrevValues.put(target, prev);
529 // KeyInfo oldKeyInfo = prevPrevValues.put(target, prev);
530 // if (oldKeyInfo != null && oldKeyInfo.keyFrame.getTime().equals(prev.keyFrame.getTime())) {
531 // System.err.println("prevPrev replaced more than once per keyFrame on " + target + "\n"
532 // + "old = " + oldKeyInfo.keyFrame.getTime() + ", " + oldKeyInfo.keyValue + "\n"
533 // + "new = " + prev.keyFrame.getTime() + ", " + prev.keyValue
534 // );
535 // }
536  }
537  }
538  KeyInfo oldPrev = prevValues.put(target, new KeyInfo(keyFrame, keyValue, prev == null));
539  if (oldPrev != null) prevPrevValues.put(target, oldPrev);
540  }
541  }
542  // Deal with ending keyValues
543  for (WritableValue target : prevValues.keySet()) {
544  KeyInfo prev = prevValues.get(target);
545  KeyInfo prevPrev = prevPrevValues.get(target);
546  if (prevPrev != null && prevPrev.keyValue.getEndValue().equals(prev.keyValue.getEndValue())) {
547  // prevPrev and prev match, so prev can be removed
548  toRemove.add(prev.keyFrame, prev.keyValue);
549  }
550  }
551  int kvRemoved = 0;
552  int kfRemoved = 0, kfTotal = timeline.getKeyFrames().size(), kfSimplified = 0, kfNotRemoved = 0;
553  // Removing unnecessary KeyValues and KeyFrames
554  List<KeyValue> newKeyValues = new ArrayList<>();
555  for (int i = 0; i < timeline.getKeyFrames().size(); i++) {
556  KeyFrame keyFrame = timeline.getKeyFrames().get(i);
557  List<KeyValue> keyValuesToRemove = toRemove.get(keyFrame);
558  if (keyValuesToRemove != null) {
559  newKeyValues.clear();
560  for (KeyValue keyValue : keyFrame.getValues()) {
561  if (keyValuesToRemove.remove(keyValue)) {
562  kvRemoved++;
563  } else {
564  if (convertToDiscrete) {
565  newKeyValues.add(new KeyValue((WritableValue)keyValue.getTarget(), keyValue.getEndValue(), Interpolator.DISCRETE));
566  } else {
567  newKeyValues.add(keyValue);
568  }
569  }
570  }
571  } else if (convertToDiscrete) {
572  newKeyValues.clear();
573  for (KeyValue keyValue : keyFrame.getValues()) {
574  newKeyValues.add(new KeyValue((WritableValue)keyValue.getTarget(), keyValue.getEndValue(), Interpolator.DISCRETE));
575  }
576  }
577  if (keyValuesToRemove != null || convertToDiscrete) {
578  if (newKeyValues.isEmpty()) {
579  if (keyFrame.getOnFinished() == null) {
580  if (keyFrame.getName() != null) {
581  System.err.println("Removed KeyFrame with name = " + keyFrame.getName());
582  }
583  timeline.getKeyFrames().remove(i);
584  i--;
585  kfRemoved++;
586  continue; // for i
587  } else {
588  kfNotRemoved++;
589  }
590  } else {
591  keyFrame = new KeyFrame(keyFrame.getTime(), keyFrame.getName(), keyFrame.getOnFinished(), newKeyValues);
592  timeline.getKeyFrames().set(i, keyFrame);
593  kfSimplified++;
594  }
595  }
596  // collecting bound targets
597  for (KeyValue keyValue : keyFrame.getValues()) {
598  WritableValue<?> target = keyValue.getTarget();
599  if (target instanceof Property) {
600  Property p = (Property) target;
601  Object bean = p.getBean();
602  if (bean instanceof Transform) {
603  bound.add((Transform) bean);
604  } else {
605  throw new UnsupportedOperationException("Bean is not transform, bean = " + bean);
606  }
607  } else {
608  throw new UnsupportedOperationException("WritableValue is not property, can't identify what it changes, target = " + target);
609  }
610  }
611  }
612 // System.out.println("bound.size() = " + bound.size());
613  System.out.printf("Removed %d (%.2f%%) repeating KeyValues out of total %d.\n", kvRemoved, 100d * kvRemoved / kvTotal, kvTotal);
614  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",
615  kfRemoved, 100d * kfRemoved / kfTotal,
616  kfSimplified, 100d * kfSimplified / kfTotal, kfTotal, kfNotRemoved, 100d * kfNotRemoved / kfTotal);
617  int check = 0;
618  for (KeyFrame keyFrame : timeline.getKeyFrames()) {
619  check += keyFrame.getValues().size();
620 // for (KeyValue keyValue : keyFrame.getValues()) {
621 // if (keyValue.getInterpolator() != Interpolator.DISCRETE) {
622 // throw new IllegalStateException();
623 // }
624 // }
625  }
626  System.out.printf("Now there are %d KeyValues and %d KeyFrames.\n", check, timeline.getKeyFrames().size());
627  }
628 
632  private void removeEmptyGroups() {
633  for (Parent p : emptyParents) {
634  Parent parent = p.getParent();
635  Group g = (Group) parent;
636  g.getChildren().addAll(p.getChildrenUnmodifiable());
637  g.getChildren().remove(p);
638  }
639  }
640 
644  private static class KeyFrameComparator implements Comparator<KeyFrame> {
645 
649  public KeyFrameComparator() {
650  }
651 
652  /* (non-Javadoc)
653  * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
654  */
655  @Override public int compare(KeyFrame o1, KeyFrame o2) {
656 // int compareTo = o1.getTime().compareTo(o2.getTime());
657 // if (compareTo == 0 && o1 != o2) {
658 // System.err.println("those two KeyFrames are equal: o1 = " + o1.getTime() + " and o2 = " + o2.getTime());
659 // }
660  return o1.getTime().compareTo(o2.getTime());
661  }
662  }
663 
664 }
Optimizer(Timeline timeline, Node root, boolean convertToDiscrete)
Definition: Optimizer.java:105