// // $Id$ // // Nenya library - tools for developing networked games // Copyright (C) 2002-2009 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/nenya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.jme.tools; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import com.jme.bounding.BoundingBox; import com.jme.bounding.BoundingSphere; import com.jme.math.FastMath; import com.jme.math.Matrix4f; import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.scene.Node; import com.jme.scene.Spatial; import com.jme.util.geom.BufferUtils; import com.samskivert.util.ObjectUtil; import com.samskivert.util.PropertiesUtil; import com.samskivert.util.StringUtil; import com.samskivert.util.Tuple; import com.threerings.jme.model.Model; import com.threerings.jme.model.ModelController; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; import com.threerings.jme.model.SkinMesh; import com.threerings.jme.util.JmeUtil; import static com.threerings.jme.Log.log; /** * An intermediate representation for models used to store data parsed from * XML and convert it into JME nodes. */ public class ModelDef { /** The base class of nodes in the model. */ public abstract static class SpatialDef { /** The node's name. */ public String name; /** The name of the node's parent. */ public String parent; /** The node's transformation. */ public float[] translation; public float[] rotation; public float[] scale; /** * Stores the names of all bones referenced by this spatial in the supplied set. */ public void getBoneNames (HashSet bones) { // nothing by default } /** Checks whether it's possible (disregarding issues of transformation) to merge * the specified spatial into this one. */ public abstract boolean canMerge (Properties props, SpatialDef other); /** Merges another spatial into this one. */ public abstract void merge (SpatialDef other, Matrix4f xform); /** Returns a JME node for this definition. */ public Spatial getSpatial (Properties props) { if (_spatial == null) { _spatial = createSpatial( PropertiesUtil.getFilteredProperties(props, name)); setTransform(); } return _spatial; } /** Sets the transform of the created node. */ protected void setTransform () { _spatial.getLocalTranslation().set(translation[0], translation[1], translation[2]); _spatial.getLocalRotation().set(rotation[0], rotation[1], rotation[2], rotation[3]); _spatial.getLocalScale().set(scale[0], scale[1], scale[2]); } /** Creates a JME node for this definition. */ public abstract Spatial createSpatial (Properties props); /** Resolves any name references using the supplied map. */ public void resolveReferences ( HashMap nodes, HashSet referenced) { Spatial pnode = nodes.get(parent); if (pnode instanceof ModelNode) { ((ModelNode)pnode).attachChild(_spatial); } else if (parent != null) { log.warning("Missing or invalid parent node [spatial=" + name + ", parent=" + parent + "]."); } } /** The JME node created for this definition. */ protected Spatial _spatial; } /** A rigid triangle mesh. */ public static class TriMeshDef extends SpatialDef { /** The geometry offset transform. */ public float[] offsetTranslation; public float[] offsetRotation; public float[] offsetScale; /** Whether or not the mesh allows back face culling. */ public boolean solid; /** The texture of the mesh, if any. */ public String texture; /** Whether or not the mesh is (partially) transparent. */ public boolean transparent; /** The vertices of the mesh. */ public HashArrayList vertices = new HashArrayList(); /** The triangle indices. */ public ArrayList indices = Lists.newArrayList(); /** Whether or not any of the vertices have texture coordinates. */ public boolean tcoords; public void addVertex (Vertex vertex) { int idx = vertices.indexOf(vertex); if (idx != -1) { indices.add(idx); } else { indices.add(vertices.size()); vertices.add(vertex); } tcoords = tcoords || vertex.tcoords != null; } // documentation inherited public boolean canMerge (Properties props, SpatialDef other) { if (getClass() != other.getClass()) { return false; // require exact class match } TriMeshDef omesh = (TriMeshDef)other; return solid == omesh.solid && transparent == omesh.transparent && ObjectUtil.equals(texture, omesh.texture) && PropertiesUtil.getSubProperties(props, name).equals( PropertiesUtil.getSubProperties(props, omesh.name)); } // documentation inherited public void merge (SpatialDef other, Matrix4f xform) { TriMeshDef omesh = (TriMeshDef)other; // prepend the inverse of the offset transformation xform = getOffsetTransform().invertLocal().multLocal(xform); // and append the other's offset xform.multLocal(omesh.getOffsetTransform()); // extract the rotation to transform normals Quaternion xrot = xform.toRotationQuat(); // transform the vertices and add them in for (Vertex vertex : omesh.vertices) { vertex.transform(xform, xrot); } for (int idx : omesh.indices) { addVertex(omesh.vertices.get(idx)); } } // documentation inherited public Spatial createSpatial (Properties props) { ModelNode node = new ModelNode(name); if (indices.size() > 0) { _mesh = createMesh(); optimizeVertexOrder(); configureMesh(props); node.attachChild(_mesh); } return node; } /** Gets the matrix representing the offset transform. */ protected Matrix4f getOffsetTransform () { Vector3f otrans = new Vector3f(), oscale = new Vector3f(1f, 1f, 1f); Quaternion orot = new Quaternion(); if (offsetTranslation != null) { otrans.set(offsetTranslation[0], offsetTranslation[1], offsetTranslation[2]); } if (offsetRotation != null) { orot.set(offsetRotation[0], offsetRotation[1], offsetRotation[2], offsetRotation[3]); } if (offsetScale != null) { oscale.set(offsetScale[0], offsetScale[1], offsetScale[2]); } return JmeUtil.setTransform(otrans, orot, oscale, new Matrix4f()); } /** Creates the mesh to attach to the node. */ protected ModelMesh createMesh () { return new ModelMesh("mesh"); } /** Reorders the vertices to optimize for vertex cache utilization. Uses the algorithm * described in Tom Forsyth's article * * Linear-Speed Vertex Cache Optimization. */ protected void optimizeVertexOrder () { // start by compiling a list of triangles cross-linked with the vertices they use // (we use a linked hash set to ensure consistent iteration order for serialization) LinkedHashSet triangles = new LinkedHashSet(); for (int ii = 0, nn = indices.size(); ii < nn; ii += 3) { Vertex[] tverts = new Vertex[] { vertices.get(indices.get(ii)), vertices.get(indices.get(ii + 1)), vertices.get(indices.get(ii + 2)) }; Triangle triangle = new Triangle(tverts); for (Vertex tvert : tverts) { if (tvert.triangles == null) { tvert.triangles = Lists.newArrayList(); } tvert.triangles.add(triangle); } triangles.add(triangle); } // init the scores for (Vertex vertex : vertices) { vertex.updateScore(Integer.MAX_VALUE); } // clear the vertices and indices to prepare for readdition vertices.clear(); indices.clear(); // while there are triangles remaining, keep adding the one with the best score // (as determined by its LRU cache position and number of remaining triangles) HashArrayList vcache = new HashArrayList(); while (!triangles.isEmpty()) { // first look for triangles in the cache Triangle bestTriangle = null; float bestScore = -1f; for (Vertex vertex : vcache) { for (Triangle triangle : vertex.triangles) { float score = triangle.getScore(); if (score > bestScore) { bestTriangle = triangle; bestScore = score; } } } // if that didn't work, scan the full list if (bestTriangle == null) { for (Triangle triangle : triangles) { float score = triangle.getScore(); if (score > bestScore) { bestTriangle = triangle; bestScore = score; } } } // add and update the vertices from the best triangle triangles.remove(bestTriangle); for (Vertex vertex : bestTriangle.vertices) { addVertex(vertex); vertex.triangles.remove(bestTriangle); vcache.remove(vertex); vcache.add(0, vertex); } // update the scores of the vertices in the cache for (int ii = 0, nn = vcache.size(); ii < nn; ii++) { vcache.get(ii).updateScore(ii); } // trim the excess (if any) from the end of the cache while (vcache.size() > 64) { vcache.remove(vcache.size() - 1); } } } /** Configures the mesh. */ protected void configureMesh (Properties props) { // set the geometry offset if (offsetTranslation != null) { _mesh.getLocalTranslation().set(offsetTranslation[0], offsetTranslation[1], offsetTranslation[2]); } if (offsetRotation != null) { _mesh.getLocalRotation().set(offsetRotation[0], offsetRotation[1], offsetRotation[2], offsetRotation[3]); } if (offsetScale != null) { _mesh.getLocalScale().set(offsetScale[0], offsetScale[1], offsetScale[2]); } // make sure texture is just a filename int sidx = (texture == null) ? -1 : Math.max(texture.lastIndexOf('/'), texture.lastIndexOf('\\')); if (sidx != -1) { texture = texture.substring(sidx + 1); } // configure using properties _mesh.configure(solid, texture, transparent, props); // set the various buffers int vsize = vertices.size(); FloatBuffer vbuf = BufferUtils.createVector3Buffer(vsize), nbuf = BufferUtils.createVector3Buffer(vsize), tbuf = tcoords ? BufferUtils.createVector2Buffer(vsize) : null; for (int ii = 0; ii < vsize; ii++) { vertices.get(ii).setInBuffers(vbuf, nbuf, tbuf); } IntBuffer ibuf = BufferUtils.createIntBuffer(indices.size()); for (int ii = 0, nn = indices.size(); ii < nn; ii++) { ibuf.put(indices.get(ii)); } _mesh.reconstruct(vbuf, nbuf, null, tbuf, ibuf); _mesh.setModelBound("sphere".equals(props.getProperty("bound")) ? new BoundingSphere() : new BoundingBox()); _mesh.updateModelBound(); // set the mesh's origin to the center of its bounding box _mesh.centerVertices(); } /** The mesh that contains the actual geometry. */ protected ModelMesh _mesh; } /** A triangle mesh that deforms according to bone positions. */ public static class SkinMeshDef extends TriMeshDef { @Override // documentation inherited public void getBoneNames (HashSet bones) { for (Vertex vertex : vertices) { bones.addAll(((SkinVertex)vertex).boneWeights.keySet()); } } @Override // documentation inherited protected ModelMesh createMesh () { return new SkinMesh("mesh"); } @Override // documentation inherited public void resolveReferences ( HashMap nodes, HashSet referenced) { super.resolveReferences(nodes, referenced); if (_mesh == null) { return; } // create and set the final weight groups SkinMesh.WeightGroup[] wgroups = new SkinMesh.WeightGroup[_groups.size()]; HashMap bones = Maps.newHashMap(); int ii = 0; int mweights = 0, tweights = 0; for (Map.Entry, WeightGroupDef> entry : _groups.entrySet()) { SkinMesh.WeightGroup wgroup = new SkinMesh.WeightGroup(); wgroup.vertexCount = entry.getValue().indices.size(); wgroup.bones = new SkinMesh.Bone[entry.getKey().size()]; int jj = 0; for (String bname : entry.getKey()) { SkinMesh.Bone bone = bones.get(bname); if (bone == null) { Spatial node = nodes.get(bname); bones.put(bname, bone = new SkinMesh.Bone((ModelNode)node)); referenced.add(node); } wgroup.bones[jj++] = bone; } wgroup.weights = toArray(entry.getValue().weights); tweights += wgroup.bones.length; mweights = Math.max(wgroup.bones.length, mweights); wgroups[ii++] = wgroup; } ((SkinMesh)_mesh).setWeightGroups(wgroups); } @Override // documentation inherited protected void configureMesh (Properties props) { // divide the vertices up by weight groups _groups = Maps.newHashMap(); for (int ii = 0, nn = vertices.size(); ii < nn; ii++) { SkinVertex svertex = (SkinVertex)vertices.get(ii); Set bones = svertex.boneWeights.keySet(); WeightGroupDef group = _groups.get(bones); if (group == null) { _groups.put(bones, group = new WeightGroupDef()); } group.indices.add(ii); for (String bone : bones) { group.weights.add(svertex.boneWeights.get(bone).weight); } } // reorder the vertices by group ArrayList overts = vertices; vertices = new HashArrayList(); int[] imap = new int[overts.size()]; for (Map.Entry, WeightGroupDef> entry : _groups.entrySet()) { for (int idx : entry.getValue().indices) { imap[idx] = vertices.size(); vertices.add(overts.get(idx)); } } for (int ii = 0, nn = indices.size(); ii < nn; ii++) { indices.set(ii, imap[indices.get(ii)]); } super.configureMesh(props); } /** The intermediate weight groups, mapped by bone names. */ protected HashMap, WeightGroupDef> _groups; } /** A generic node. */ public static class NodeDef extends SpatialDef { // documentation inherited public boolean canMerge (Properties props, SpatialDef other) { return false; } // documentation inherited public void merge (SpatialDef other, Matrix4f xform) { throw new UnsupportedOperationException(); } // documentation inherited public Spatial createSpatial (Properties props) { return new ModelNode(name); } } /** Represents a triangle for processing purposes. */ public static class Triangle { public Vertex[] vertices; public Triangle (Vertex[] vertices) { this.vertices = vertices; } public float getScore () { return vertices[0].score + vertices[1].score + vertices[2].score; } } /** A basic vertex. */ public static class Vertex { public float[] location; public float[] normal; public float[] tcoords; public ArrayList triangles; public float score; public void updateScore (int cacheIdx) { float pscore; if (cacheIdx > 63) { pscore = 0f; // outside the cache } else if (cacheIdx < 3) { pscore = 0.75f; // the three most recent vertices } else { pscore = FastMath.pow((63 - cacheIdx) / 60f, 1.5f); } score = pscore + 2f * FastMath.pow(triangles.size(), -0.5f); } public void transform (Matrix4f xform, Quaternion xrot) { Vector3f xvec = new Vector3f(location[0], location[1], location[2]); xform.mult(xvec, xvec); location[0] = xvec.x; location[1] = xvec.y; location[2] = xvec.z; xvec.set(normal[0], normal[1], normal[2]); xrot.mult(xvec, xvec); normal[0] = xvec.x; normal[1] = xvec.y; normal[2] = xvec.z; } public void setInBuffers ( FloatBuffer vbuf, FloatBuffer nbuf, FloatBuffer tbuf) { vbuf.put(location); nbuf.put(normal); if (tbuf != null) { if (tcoords != null) { tbuf.put(tcoords); } else { tbuf.put(0f); tbuf.put(0f); } } } public String toString () { return StringUtil.toString(location); } @Override // documentation inherited public int hashCode () { return Arrays.hashCode(location) ^ Arrays.hashCode(normal) ^ Arrays.hashCode(tcoords); } @Override // documentation inherited public boolean equals (Object obj) { Vertex overt = (Vertex)obj; return Arrays.equals(location, overt.location) && Arrays.equals(normal, overt.normal) && Arrays.equals(tcoords, overt.tcoords); } } /** A vertex influenced by a number of bones. */ public static class SkinVertex extends Vertex { /** The bones influencing the vertex, mapped by name. */ public HashMap boneWeights = Maps.newHashMap(); public void addBoneWeight (BoneWeight weight) { if (weight.weight == 0f) { return; } BoneWeight bweight = boneWeights.get(weight.bone); if (bweight != null) { bweight.weight += weight.weight; } else { boneWeights.put(weight.bone, weight); } } /** Finds the bone nodes influencing this vertex. */ public HashSet getBones (HashMap nodes) { HashSet bones = Sets.newHashSet(); for (String bone : boneWeights.keySet()) { Spatial node = nodes.get(bone); if (node instanceof ModelNode) { bones.add((ModelNode)node); } else { log.warning("Missing or invalid bone for bone weight " + "[bone=" + bone + "]."); } } return bones; } /** Returns the weight of the given bone. */ public float getWeight (ModelNode bone) { BoneWeight bweight = boneWeights.get(bone.getName()); return (bweight == null) ? 0f : bweight.weight; } } /** The influence of a bone on a vertex. */ public static class BoneWeight { /** The name of the influencing bone. */ public String bone; /** The amount of influence. */ public float weight; } /** A group of vertices influenced by the same bone. */ public static class WeightGroupDef { /** The indices of the affected vertex. */ public ArrayList indices = Lists.newArrayList(); /** The interleaved vertex weights. */ public ArrayList weights = Lists.newArrayList(); } /** Contains the transform of a node for preprocessing. */ public static class TransformNode extends Node { /** The source definition. */ public SpatialDef spatial; /** If true, this node is referenced by name (as a bone or parent) and cannot be merged * into another. */ public boolean referenced; /** If true, this node is a controller target; nodes beneath it can only be merged with * other descendants. */ public boolean controlled; /** The node's current local transform. */ public Matrix4f localTransform = new Matrix4f(); /** The node's current world space transform. */ public Matrix4f worldTransform = new Matrix4f(); /** The node's local transform in the original model, or null if the node's * transform has diverged from the original. */ public Matrix4f baseLocalTransform; /** The relative transforms between this and all other loosely compatible nodes not yet * eliminated. As soon as the relative transform diverges in the course of preprocessing * an animation, the node/transform pair is removed from the list. */ public ArrayList> relativeTransforms; /** Marks this node as having been transformed in the course of an animation. */ public boolean transformed; public TransformNode (SpatialDef spatial) { super(spatial.name); this.spatial = spatial; setLocalTransform(spatial.translation, spatial.rotation, spatial.scale); } public void setLocalTransform (float[] translation, float[] rotation, float[] scale) { getLocalTranslation().set(translation[0], translation[1], translation[2]); getLocalRotation().set(rotation[0], rotation[1], rotation[2], rotation[3]); getLocalScale().set(scale[0], scale[1], scale[2]); JmeUtil.setTransform( getLocalTranslation(), getLocalRotation(), getLocalScale(), localTransform); } @Override // documentation inherited public void updateWorldVectors () { super.updateWorldVectors(); JmeUtil.setTransform( getWorldTranslation(), getWorldRotation(), getWorldScale(), worldTransform); } public boolean canMerge (Properties props, TransformNode onode) { // nodes must have same controlled ancestor return !onode.referenced && spatial.canMerge(props, onode.spatial) && getControlledAncestor() == onode.getControlledAncestor(); } protected Node getControlledAncestor () { Node ref = this; while (ref instanceof TransformNode && !((TransformNode)ref).controlled) { ref = ref.getParent(); } return ref; } public void cullDivergentTransforms () { if (baseLocalTransform != null && !epsilonEquals(localTransform, baseLocalTransform)) { baseLocalTransform = null; } for (Iterator> it = relativeTransforms.iterator(); it.hasNext(); ) { Tuple tuple = it.next(); if (!epsilonEquals(getRelativeTransform(tuple.left), tuple.right)) { it.remove(); } } } public Matrix4f getRelativeTransform (TransformNode other) { // return the matrix that takes vertices from the space of the other node // into the space of this one Matrix4f inv = new Matrix4f(); worldTransform.invert(inv); return inv.mult(other.worldTransform); } } /** The meshes and bones comprising the model. */ public ArrayList spatials = Lists.newArrayList(); public void addSpatial (SpatialDef spatial) { // put nodes before meshes so that bones are updated before skin spatials.add(spatial instanceof NodeDef ? 0 : spatials.size(), spatial); } /** * Creates and returns a transform tree representing the model for preprocessing. */ public Node createTransformTree (Properties props, HashMap nodes) { // create the nodes and map them by name for (SpatialDef spatial : spatials) { nodes.put(spatial.name, new TransformNode(spatial)); } // resolve the parents and collect the names of the bones Node root = new Node("root"); HashSet bones = Sets.newHashSet(); for (TransformNode node : nodes.values()) { if (node.spatial.parent == null) { root.attachChild(node); } else { TransformNode pnode = nodes.get(node.spatial.parent); if (pnode != null) { pnode.attachChild(node); pnode.referenced = true; } } node.spatial.getBoneNames(bones); } // mark the bones as referenced for (String name : bones) { TransformNode node = nodes.get(name); if (node != null) { node.referenced = true; } } // mark the controlled nodes String[] controllers = StringUtil.parseStringArray(props.getProperty("controllers", "")); for (String controller : controllers) { Properties subProps = PropertiesUtil.getSubProperties(props, controller); TransformNode node = nodes.get(subProps.getProperty("node", controller)); if (node != null) { node.referenced = node.controlled = true; } } // store the base transforms and relative transforms for merge candidates root.updateGeometricState(0f, true); for (TransformNode node : nodes.values()) { node.baseLocalTransform = new Matrix4f(node.localTransform); node.relativeTransforms = Lists.newArrayList(); for (TransformNode onode : nodes.values()) { if (node == onode || !node.canMerge(props, onode)) { continue; } node.relativeTransforms.add(new Tuple( onode, node.getRelativeTransform(onode))); } } return root; } /** * Merges compatible meshes that retain the same relative transform throughout all animations. */ public void mergeSpatials (HashMap nodes) { for (TransformNode node : nodes.values()) { if (!spatials.contains(node.spatial)) { continue; } for (Tuple tuple : node.relativeTransforms) { if (spatials.contains(tuple.left.spatial)) { node.spatial.merge(tuple.left.spatial, tuple.right); spatials.remove(tuple.left.spatial); } } } } /** * Creates the model node defined herein. * * @param props the properties of the model * @param nodes a node map to populate */ public Model createModel (Properties props, HashMap nodes) { Model model = new Model(props.getProperty("name", "model"), props); // start by creating the spatials and mapping them to their names for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { Spatial spatial = spatials.get(ii).getSpatial(props); nodes.put(spatial.getName(), spatial); } // then go through again, resolving any name references and attaching root children HashSet referenced = Sets.newHashSet(); for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { SpatialDef sdef = spatials.get(ii); sdef.resolveReferences(nodes, referenced); if (sdef.getSpatial(props).getParent() == null) { model.attachChild(sdef.getSpatial(props)); } } // create any controllers listed String[] controllers = StringUtil.parseStringArray( props.getProperty("controllers", "")); for (int ii = 0; ii < controllers.length; ii++) { Properties subProps = PropertiesUtil.getSubProperties(props, controllers[ii]); String node = subProps.getProperty("node", controllers[ii]); Spatial target = node.equals(model.getName()) ? model : nodes.get(node); if (target == null) { log.warning("Missing controller node [name=" + node + "]."); continue; } ModelController ctrl = createController(subProps, target); if (ctrl != null) { model.addController(ctrl); referenced.add(target); } } // get rid of any nodes that serve no purpose pruneUnusedNodes(model, nodes, referenced); // set the overall scale model.setLocalScale(Float.parseFloat(props.getProperty("scale", "1"))); return model; } /** Creates, configures, and returns a model controller. */ protected ModelController createController ( Properties props, Spatial target) { // attempt to create an instance of the controller ModelController ctrl; String cname = props.getProperty("class", ""); try { ctrl = (ModelController)Class.forName(cname).newInstance(); } catch (Exception e) { log.warning("Error instantiating controller [class=" + cname + ", error=" + e + "]."); return null; } ctrl.configure(props, target); return ctrl; } /** Recursively removes any unused nodes. */ protected boolean pruneUnusedNodes ( ModelNode node, HashMap nodes, HashSet referenced) { boolean hasValidChildren = false; for (int ii = node.getQuantity() - 1; ii >= 0; ii--) { Spatial child = node.getChild(ii); if (!(child instanceof ModelNode) || pruneUnusedNodes((ModelNode)child, nodes, referenced)) { hasValidChildren = true; } else { node.detachChildAt(ii); nodes.remove(child.getName()); } } return referenced.contains(node) || hasValidChildren; } /** Determines whether a pair of matrices are "close enough" to equal. */ public static boolean epsilonEquals (Matrix4f m1, Matrix4f m2) { for (int ii = 0; ii < 4; ii++) { for (int jj = 0; jj < 4; jj++) { if (FastMath.abs(m1.get(ii, jj) - m2.get(ii, jj)) > 0.0001f) { return false; } } } return true; } /** Converts a boxed Integer list to an unboxed int array. */ protected static int[] toArray (ArrayList list) { int[] array = new int[list.size()]; for (int ii = 0, nn = list.size(); ii < nn; ii++) { array[ii] = list.get(ii); } return array; } /** Converts a boxed Float list to an unboxed float array. */ protected static float[] toArray (ArrayList list) { float[] array = new float[list.size()]; for (int ii = 0, nn = list.size(); ii < nn; ii++) { array[ii] = list.get(ii); } return array; } /** Accelerates {@link ArrayList#indexOf}, {@link ArrayList#contains}, and * {@link ArrayList#remove} using an internal hash map (assumes that all elements of the list * are unique and non-null). */ protected static class HashArrayList extends ArrayList { @Override // documentation inherited public boolean add (E element) { add(size(), element); return true; } @Override // documentation inherited public void add (int idx, E element) { super.add(idx, element); remapFrom(idx); } @Override // documentation inherited public E remove (int idx) { E element = super.remove(idx); _indices.remove(element); remapFrom(idx); return element; } @Override // documentation inherited public void clear () { super.clear(); _indices.clear(); } @Override // documentation inherited public int indexOf (Object obj) { Integer idx = _indices.get(obj); return (idx == null ? -1 : idx); } @Override // documentation inherited public boolean contains (Object obj) { return _indices.containsKey(obj); } @Override // documentation inherited public boolean remove (Object obj) { Integer idx = _indices.remove(obj); if (idx != null) { super.remove(idx); return true; } else { return false; } } protected void remapFrom (int idx) { for (int ii = idx, nn = size(); ii < nn; ii++) { _indices.put(get(ii), ii); } } /** Maps elements to their indices in the list. */ protected HashMap _indices = Maps.newHashMap(); } }