package HumanoidAnimation.Bones;

import java.io.File;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import org.web3d.x3d.jsail.X3DConcreteNode;
import org.web3d.x3d.jsail.X3DLoaderDOM;
import org.web3d.x3d.jsail.Core.CommentsBlock;
import org.web3d.x3d.jsail.Core.meta;
import org.web3d.x3d.jsail.Core.X3D;
import org.web3d.x3d.jsail.Geometry3D.IndexedFaceSet;
import org.web3d.x3d.jsail.Grouping.Transform;
import org.web3d.x3d.jsail.fields.MFVec3f;
import org.web3d.x3d.jsail.fields.SFVec3f;
import org.web3d.x3d.jsail.HAnim.HAnimHumanoid;
import org.web3d.x3d.jsail.HAnim.HAnimJoint;
import org.web3d.x3d.jsail.HAnim.HAnimSegment;
import org.web3d.x3d.jsail.Networking.Inline;
import org.web3d.x3d.jsail.Rendering.Coordinate;
import org.web3d.x3d.jsail.Rendering.TriangleSet;
import org.web3d.x3d.sai.Core.X3DNode;

/**
 * Examine visualization axes for each bone to compute HAnimJOint center offsets in a corresponding HAnimHumanoid skeleton model,
 * enabling proper rotations when the parent skeleton is animated.
 * @see <a href="ComputeBoneOffsetsForJointCentersFlowchart.png" target="_blank">ComputeBoneOffsetsForJointCentersFlowchart.png</a>
 * @see <a href="BuildingLOA5AnimatedHumanoidsUsingHAnimX3D.pptx" target="_blank">BuildingLOA5AnimatedHumanoidsUsingHAnimX3D.pptx</a>
 * @author Don Brutzman
 */

public class ComputeBoneOffsetsForJointCenters
{
	/** Default constructor to create this object. */
	public ComputeBoneOffsetsForJointCenters ()
	{
        // default constructor
	}
    /** main() method is used for command-line or build script invocation of this class
     * @param args invocation arguments (none yet defined)
     */
    public static void main(String args[])
    {
        System.out.println("Build this X3D model using X3DJSAIL build " + X3D.X3DJSAIL_VERSION_DATE + ", now processing Bones models...");
        
        ComputeBoneOffsetsForJointCenters thisProgram = new ComputeBoneOffsetsForJointCenters();
        thisProgram.run();
        
        System.out.println("Processing Bones models complete.");
    }
    
    /** Top-most object containing both head and   (and thus everything else) */
    X3D        boneModelX3D;
    File       boneModelFile;
    String     boneModelName        = "c1"; // no .x3d extension
    boolean hasBoneIfsCoordIndex    = false;
    boolean hasBoneCoordinatePoints = false;
    
    X3D    skeletonModelX3D;
    File   skeletonModelFile;
    String skeletonModelName        = "AllBonesLOA5SkeletonsInlineAnimation"; // no .x3d extension
    String skeletonModelPath        = "Bones/" + skeletonModelName + ".x3d";
    HAnimHumanoid humanoidNode;
    String        humanoidNodeDEF = "hanim_AllBonesSkeleton";
    ArrayList<X3DNode> skeletonJointList;
    int hierarchyLevel = 0;
    
    X3DConcreteNode nextNode;
    HAnimJoint      nextJoint;
    HAnimSegment    nextSegment;
    Transform       boneMeshTransform;
    Transform     axisOffsetTransform;
    float[]       jointCenterAdjustmentArray = {0.0f, 0.0f, 0.0f};
    SFVec3f       jointCenterAdjustment;
    CommentsBlock jointCenterComputationCommentsBlock = new CommentsBlock();
    
    int        jointCount = 0;
    int      segmentCount = 0;
    int totalModificationCount = 0;
    DecimalFormat formatPrecision4 = new DecimalFormat ("+#0.0000;-#0.0000");
    

	/** Run this program: retrieve skeleton model, process bones */
	public void run()
    {
        try
        {
//          processBone(boneModelName); // test default
            System.out.println();
            getSkeletonModel();
            traverseSkeletonModel();
            System.out.println();
            // apply modifications, log results, and save model by overwriting original
            humanoidNode.setSkeleton(skeletonJointList);
            System.out.println(humanoidNode.toStringX3D());
            System.out.println("Saving result to " + skeletonModelPath);
            skeletonModelX3D.toFileX3D(skeletonModelPath); // overwrite original
            System.out.println();
            System.out.println("totalModifiedJointCount=" + totalModificationCount + " out of " + jointCount + " HAnimJoint nodes found in this skeleton");
        }
        catch (Exception e) // catchall precaution
        {
            System.out.println("run() catchall exception: " + e.getClass() + " " + e.getMessage());
            e.printStackTrace();
            System.out.flush();
            System.err.flush();
        }
    }
    
	/** Recurse to process all joints in tree
     * @param nextJoint next HAnimJoint for processing
     */
	public void traverseNextJoint(HAnimJoint nextJoint)
    {
        String    indentSpace = "    ";
        String hierarchyIndent = new String();
        
        hierarchyLevel++;
        for (int i = 0; i < hierarchyLevel; i++) // indent
            hierarchyIndent += " ";
        
        jointCount++;
        System.out.println("=================================================================================");
        System.out.println("HAnimJoint #" + jointCount+ ", hierarchy level " + hierarchyLevel);
        
        System.out.println(hierarchyIndent + 
//                             "level=" + hierarchyLevel + hierarchySpace +
//                        " nextJoint=" + 
                                  nextJoint.getElementName() + " DEF='" + nextJoint.getDEF() + "' name='" + nextJoint.getName() +
                            "' center='" + SFVec3f.toString(nextJoint.getCenter()) + 
                       "' translation='" + SFVec3f.toString(nextJoint.getTranslation()) + "'" + 
                             " parent="  + nextJoint.getParent().getElementName()); // TODO  + " parentDEF='" + ((X3DNode) nextJoint.getParent()).getDEF()
        
        // tail recursion
        for (X3DNode childNode : nextJoint.getChildrenList())
        {
            if      (childNode instanceof CommentsBlock)
            {
                CommentsBlock comments = (CommentsBlock) childNode;
                System.out.println(hierarchyIndent + " " + comments.toStringX3D().trim());
            }
            else if (childNode instanceof HAnimSegment)
            {
                nextSegment = (HAnimSegment) childNode;
                segmentCount++;
                System.out.println("- - - - - - - - - - ");
                System.out.println("HAnimSegment #" + segmentCount);
                System.out.println(hierarchyIndent + " " + 
//                        "nextSegment=" + 
                        nextSegment.getElementName() + " DEF='" + nextSegment.getDEF() + "' name='" + nextSegment.getName() +
                            "' " + nextSegment.getDescription());
                
                for (X3DNode hanimSegmentChildNode : nextSegment.getChildrenList())
                {
                    if (hanimSegmentChildNode instanceof Inline)
                    {
                        Inline hanimSegmentInline = (Inline)hanimSegmentChildNode;
                        System.out.println(hierarchyIndent + "  " + 
//                        "hanimSegmentInline=" + 
                        hanimSegmentInline.getElementName() + " DEF='" + hanimSegmentInline.getDEF() + "' url='" + hanimSegmentInline.getUrlList().get(0) + "'");
                        boneModelName = hanimSegmentInline.getUrlString().trim();
                        if (!boneModelName.isBlank())
                        {
                            boneModelName = boneModelName.replace("'","").replace("\"","");
                            boneModelName = boneModelName.substring(0,boneModelName.indexOf(".x3d"));
                        }
//                        if (boneModelName.equals("sacrum"))
//                            boneModelName = "sacrum_bone";
                        System.out.println("boneModelName=" + boneModelName);
                        boolean foundBoneModel = processBone(boneModelName);
                        
                        // TODO be careful not to make modifications if they have already been applied
                        boolean adjustmentsAlreadyApplied = false;
                        if (foundBoneModel)
                        {
                            // look at contained comments within HAnimSegment to determine if parent HAnimJoint was already modified
                            for (X3DNode nextChildNode : nextSegment.getChildren())
                            {
                                if (nextChildNode instanceof CommentsBlock)
                                {
                                    CommentsBlock foundComment = (CommentsBlock)nextChildNode;
                                    if (foundComment.toStringX3D().contains("=joint center"))
                                    {
                                        adjustmentsAlreadyApplied = true;
                                        break;
                                    }
                                }
                            }
                        }
                        if (adjustmentsAlreadyApplied)
                        {
                            totalModificationCount++;
                            System.out.println(nextJoint.getElementName() + "[name=" + nextJoint.getName() + "] center modifications have already been applied, no changes made");
                            System.out.println("  " + jointCenterComputationCommentsBlock.toStringX3D(/*indent*/ 2).trim()); 
                        }
                        else if (foundBoneModel && !adjustmentsAlreadyApplied)
                        {
                            totalModificationCount++;
                            if ((jointCenterComputationCommentsBlock != null) && !jointCenterComputationCommentsBlock.isEmpty())
                            {
                                // TODO update the skeleton model for parent HAnimJoint
                                SFVec3f originalCenter = new SFVec3f(nextJoint.getCenter());
                                System.out.println("Adjustments made to " + nextJoint.getElementName() + "[name=" + nextJoint.getName() + "] by traverseNextJoint():");
                                System.out.println("original " + nextJoint.getElementName() + "[name=" + nextJoint.getName() + "].getCenter()=(" + SFVec3f.toString(nextJoint.getCenter()) + ")");
                                System.out.println("adjusted " + nextJoint.getElementName() + "[name=" + nextJoint.getName() + "].setCenter  =(" + jointCenterAdjustment + ")");
                                nextJoint.setCenter(jointCenterAdjustment);
                                System.out.print  ("  result=" + nextJoint.getElementName() + "[name=" + nextJoint.getName() + "].getCenter()=(" + SFVec3f.toString(nextJoint.getCenter()) + ")");
                                if  (originalCenter.equals(jointCenterAdjustment))
                                     System.out.println(" (no change)");
                                else System.out.println();
                        
                                if (!hasBoneIfsCoordIndex || !hasBoneCoordinatePoints)
                                     System.out.print("adjusted ");
                                else System.out.print("**modify ");
                                System.out.println(nextJoint.getElementName() + "[name=" + nextJoint.getName() + "] by adding jointCenterComputationComments:");
                                System.out.println(jointCenterComputationCommentsBlock.toStringX3D());
                                
                                nextJoint.clearComments(); // this method acts carefully! comments may be mixed in with other nodes
                                nextJoint.addComments(jointCenterComputationCommentsBlock);
                            }
                        }
                    }
                }
            }
            else if (childNode instanceof HAnimJoint)
            {
                HAnimJoint childJoint = (HAnimJoint) childNode;
                traverseNextJoint(childJoint);
            }
            else // X3DConcreteNode, unlikely but if found then a ProtoInstance
            {
                nextNode = (X3DConcreteNode) childNode;
                System.out.println("nextNode=" + nextNode.getElementName() + "' DEF='" + nextNode.getDEF() + "' name='" + nextNode.getName() + 
                               " parent="  + nextJoint.getParent().getElementName());
                // TODO create an HAnimJoint from this protoInstance?
            }
        }
        hierarchyLevel--; 
    }
    
	/** Traverse joints in skeleton model, starting at the top and then recursing
     * @return whether successful */
	public boolean traverseSkeletonModel()
    {
        HAnimJoint topJoint;
        skeletonJointList = humanoidNode.getSkeletonList(); // start at the top
        
        for (X3DNode childNode : skeletonJointList)
        {
            if (childNode instanceof HAnimJoint)
            {
                topJoint = (HAnimJoint) childNode;
                traverseNextJoint(topJoint);
            }
            else // X3DConcreteNode, unlikely but if found then a ProtoInstance
            {
                nextNode = (X3DConcreteNode) childNode;
                System.out.println("nextNode=" + nextNode.getElementName() + "' DEF='" + nextNode.getDEF() + "' name='" + nextNode.getName() + 
                               " parent="  + nextJoint.getParent().getElementName());
                // TODO create an HAnimJoint from this protoInstance?  unlikely, might vary too much.
            }
        }
        System.out.println("=================================================================================");
        
        return true;
    }
    
	/** Retrieve skeleton model
     * @return whether successful */
	public boolean getSkeletonModel()
    {
        System.out.println("skeletonModelPath=" + skeletonModelPath);
        skeletonModelX3D  = new X3D  (skeletonModelPath);
        skeletonModelFile = new File (skeletonModelPath);
        if (!skeletonModelFile.exists())
        {
            System.out.println("skeletonModelFile=" + skeletonModelFile.getPath() + " does not exist");
            return false;
        }
        System.out.println("getSkeletonModel() received X3D node: <" + skeletonModelX3D.getElementName() + "' profile='" + skeletonModelX3D.getProfile()  + "' version='" + skeletonModelX3D.getVersion() + "'>");
        nextNode = skeletonModelX3D.findNodeByDEF(humanoidNodeDEF); // HAnimHumanoid
//      System.out.println("<" + nextNode.getElementName() + " DEF='" + nextNode.getDEF() +"' name='" + nextnextNodeJoint.getName() +"'>"); // debug
        if (nextNode instanceof HAnimHumanoid)
            humanoidNode = (HAnimHumanoid) nextNode;
        else
        {
            System.out.println("HAnimHumanoid DEF='" + humanoidNodeDEF + "' not found");
            return false;
        }
        System.out.println("found humanoidNode: <" + humanoidNode.getElementName() + " DEF='" + humanoidNode.getDEF() + "' loa='" + humanoidNode.getLoa() + 
                                 "' name='" + humanoidNode.getName() + "' version='" + humanoidNode.getVersion() + "'>");
//      System.out.println(humanoidNode.toStringX3D());

        return true;
    }
	/** Retrieve bone model
     * @param boneModelPath location of next bone model
     * @return whether successful */
	public boolean getBoneModel(String boneModelPath)
    {
        System.out.println("boneModelPath=" + boneModelPath);
        boneModelFile = new File (boneModelPath);
        if (!boneModelFile.exists())
        {
            System.out.println("boneModelFile=" + boneModelFile.getPath() + " does not exist");
            return false;
        }
		X3DLoaderDOM x3dLoaderDOM = new X3DLoaderDOM();
		x3dLoaderDOM.loadModelFromFileX3D(boneModelPath);
        if (x3dLoaderDOM.isLoadSuccessful())
		{
            System.out.println(boneModelPath + " x3dLoaderDOM.isLoadSuccessful()=" + x3dLoaderDOM.isLoadSuccessful());
			if (x3dLoaderDOM.getX3dObjectTree() instanceof X3D)
			{
                System.out.println("[debug] x3dLoaderDOM.getX3dObjectTree() instanceof X3D)=" + (x3dLoaderDOM.getX3dObjectTree() instanceof X3D));
                boneModelX3D = (X3D) x3dLoaderDOM.getX3dObjectTree();
            }
        }
        return true;
    }

	/** Find relevant bone structures and produce output comments
     * @param boneModelName name of bone, with no .x3d extension
     * @return whether successful */
	public boolean processBone(String boneModelName)
    {
        String boneModelPath       = "Bones/" + boneModelName + ".x3d";
        
        boolean foundBone = getBoneModel(boneModelPath);
        if (foundBone)
        {
            hasBoneIfsCoordIndex    = false;
            hasBoneCoordinatePoints = false;
            String geometryDEF = boneModelName + "_geometry";
            X3DConcreteNode geometryNode;
            if (boneModelX3D.hasNodeByDEF(geometryDEF))
            {
                X3DConcreteNode foundNode = boneModelX3D.findNodeByDEF(geometryDEF);
                if      (foundNode instanceof IndexedFaceSet)
                {
                     geometryNode = (IndexedFaceSet) foundNode;
                     hasBoneIfsCoordIndex = (((IndexedFaceSet)geometryNode).getCoordIndexString().length() > 0);
                     System.out.println("[debug]    hasBoneIfsCoordIndex=" + hasBoneIfsCoordIndex + " DEF='" + geometryNode.getDEF() +
                                            "' IndexedFaceSet.getCoordIndex()=" + 
                                            ((IndexedFaceSet)geometryNode).getCoordIndexString());
                }
                else if (foundNode instanceof TriangleSet)
                {
                     geometryNode = (TriangleSet) foundNode;
                     System.out.println("[debug] DEF='" + geometryNode.getDEF() + "' TriangleSet");
                }
                else if (foundNode != null) // unexpected node type found
                {
                    System.out.println("[debug] DEF='" + foundNode.getDEF() + "' " + foundNode.getElementName() + " is unexpected node type");
                }
            }
            else System.out.println("[debug] geometryNode DEF='" + geometryDEF + "' not found!");
          
            String coordinateDEF = boneModelName + "_coordinate";
            if (boneModelX3D.hasNodeByDEF(coordinateDEF))
            {
                Coordinate coordinateNode;
                X3DConcreteNode foundNode = boneModelX3D.findNodeByDEF(coordinateDEF);
                if      (foundNode instanceof Coordinate)
                {
                    coordinateNode = (Coordinate) foundNode;
                    hasBoneCoordinatePoints = (((Coordinate)coordinateNode).getPoint().length > 0);
                    System.out.println("[debug] hasBoneCoordinatePoints=" +  hasBoneCoordinatePoints + " DEF='" + coordinateNode.getDEF() +
                                             "' coordinateNode.getPoint()=" + 
                                        (new MFVec3f(((Coordinate)coordinateNode).getPoint())).toString());
                }
                else if (foundNode != null) // unexpected node type found
                {
                    System.out.println("[debug] DEF='" + foundNode.getDEF() + "' " + foundNode.getElementName() + " is unexpected node type");
                }
            }
            else System.out.println("[debug] Coordinate DEF='" + coordinateDEF + "' not found!");
            
            // begin processing
            nextNode = boneModelX3D.findNodeByDEF(boneModelName);
            if  (nextNode instanceof Transform)
                 boneMeshTransform = (Transform) nextNode;
            else 
            {
                System.out.println("*** boneMeshTransform Transform DEF='" + boneModelName + "' not found");
                return false; // not found
            }
            nextNode = boneModelX3D.findNodeByDEF("CenterOfRotationForJoint");
            if  (nextNode instanceof Transform)
                 axisOffsetTransform = (Transform) nextNode;
            else 
            {
                System.out.println("*** axisOffsetTransform Transform DEF='" + "CenterOfRotationForJoint" + "' not found");
                return false; // not found
            }
            
            System.out.println("Test computations and outputs for " + boneModelName +".x3d parameters follow:");
            System.out.println("    boneMeshTransform.translation=" + new SFVec3f(  boneMeshTransform.getTranslation()).toString());
            System.out.println("  axisOffsetTransform.translation=" + new SFVec3f(axisOffsetTransform.getTranslation()).toString());
            
            if ((axisOffsetTransform.getTranslation()[0] == 0) &&
                (axisOffsetTransform.getTranslation()[1] == 0) &&
                (axisOffsetTransform.getTranslation()[2] == 0))
            {
                System.out.println("No axisOffsetTransform nonzero translation value found, thus no computation for change in joint center is possible.");
                System.out.println();
                return false;
            }
            else
            {
                System.out.println("Found axisOffsetTransform translation value, now computing change in for parent joint center.");
            }
            // compute non-zero offset for HAnimJoint center when axisOffsetTransform is non-zero
            jointCenterAdjustmentArray[0] = boneMeshTransform.getTranslation()[0] + axisOffsetTransform.getTranslation()[0];
            jointCenterAdjustmentArray[1] = boneMeshTransform.getTranslation()[1] + axisOffsetTransform.getTranslation()[1];
            jointCenterAdjustmentArray[2] = boneMeshTransform.getTranslation()[2] + axisOffsetTransform.getTranslation()[2];
            
            // alternate, simpler handling of SFVec3f addition
            jointCenterAdjustment =   new SFVec3f();                                      // reset too new object, 0 0 0
            jointCenterAdjustment.add(new SFVec3f(  boneMeshTransform.getTranslation())); // reset
            jointCenterAdjustment.add(new SFVec3f(axisOffsetTransform.getTranslation())); // add
            jointCenterAdjustmentArray = jointCenterAdjustment.getPrimitiveValue();
            System.out.println("  jointCenterTranslation='" +             jointCenterAdjustment +                  "'        (pairwise addition)");
            System.out.println("  jointCenterTranslation='" + new SFVec3f(jointCenterAdjustmentArray).toString() + "'        (SFVec3f  addition)");
            
            System.out.println("  jointCenterTranslation='" + 
                                  formatPrecision4.format(jointCenterAdjustmentArray[0]) + " " + 
                                  formatPrecision4.format(jointCenterAdjustmentArray[1]) + " " + 
                                  formatPrecision4.format(jointCenterAdjustmentArray[2]) + "'" + " (DecimalFormat with leading signum for consistent comment spacing)");
            
            // format as XML comments for copy/paste
            // <!-- vertebra c2.x3d + translation +0.0000 +1.6081 -0.0435 -->
            // <!-- vertebra c2.x3d adjusted axes +0.0000 -0.0120 +0.0070 -->
            // <!-- vertebra c2.x3d =joint center +0.0000 +1.5961 -0.0365 -->
            String comment0 = "computations follow from ComputeBoneOffsetsForJointCenters for parent joint center of rotation";
            String comment1 = "bone " + boneModelName + ".x3d + translation " + 
                               formatPrecision4.format(boneMeshTransform.getTranslation()[0]) + " " + 
                               formatPrecision4.format(boneMeshTransform.getTranslation()[1]) + " " + 
                               formatPrecision4.format(boneMeshTransform.getTranslation()[2]);
            String comment2 = "bone " + boneModelName + ".x3d adjusted axes " + 
                               formatPrecision4.format(axisOffsetTransform.getTranslation()[0]) + " " + 
                               formatPrecision4.format(axisOffsetTransform.getTranslation()[1]) + " " + 
                               formatPrecision4.format(axisOffsetTransform.getTranslation()[2]);
            String comment3 = "bone " + boneModelName + ".x3d =joint center " + 
                               formatPrecision4.format(jointCenterAdjustment.getPrimitiveValue()[0]) + " " + 
                               formatPrecision4.format(jointCenterAdjustment.getPrimitiveValue()[1]) + " " + 
                               formatPrecision4.format(jointCenterAdjustment.getPrimitiveValue()[2]);
            
            jointCenterComputationCommentsBlock.clear(); // ensure reset
            jointCenterComputationCommentsBlock = new CommentsBlock(new String[] {comment0, comment1, comment2, comment3});
            
//            System.out.println("Existing comments within axisOffsetTransform:");
//            for (CommentsBlock nextCommentsBlock : axisOffsetTransform.getCommentsBlockList())
//                 System.out.println(nextCommentsBlock.toStringX3D(/*indentLevel*/ 2).trim());
//            if ((axisOffsetTransform.getCommentsBlockList().size() == 1) &&
//                 axisOffsetTransform.getCommentsBlockList().get(0).toStringX3D().contains("insert computations for joint center of rotation here"))
//                 axisOffsetTransform.clearComments(); //  remove original single default comment
            
            if (hasBoneIfsCoordIndex && hasBoneCoordinatePoints)
            {
                // update bone model: comments, meta modification date, meta X3DJSAIL
                axisOffsetTransform.setComments(jointCenterComputationCommentsBlock); // replaces prior comments
                System.out.println("Updated comments within axisOffsetTransform:");
                for (CommentsBlock nextCommentsBlock : axisOffsetTransform.getCommentsBlockList())
                     System.out.println(nextCommentsBlock.toStringX3D(/*indentLevel*/ 2).trim());

                Date date = new Date(System.currentTimeMillis());
                SimpleDateFormat dateFormat = new SimpleDateFormat("d MMMM yyyy");
                if (boneModelX3D.getHead().hasMeta(meta.NAME_MODIFIED))
                {
                    boneModelX3D.getHead().findMetaByName(meta.NAME_MODIFIED).setContent(dateFormat.format(date));
                }
                else boneModelX3D.getHead().addMeta(new meta(meta.NAME_MODIFIED, dateFormat.format(date)));
                     boneModelX3D.toFileX3D(boneModelPath); // overwrite original  

    //            System.out.println("<HAnimJoint DEF='hanim_" + boneModelName +"' name='" + boneModelName +"'>");
                System.out.println("Comments for addition in HAnimHumanoid skeleton, in between corresponding HAnimSegment and Inline:");
                System.out.println("<HAnimSegment DEF='hanim_" + boneModelName +"' name='" + boneModelName +"'>");
                System.out.println("  " + jointCenterComputationCommentsBlock.toStringX3D(/*indent*/ 2).trim());
                System.out.println("  <Inline DEF='" + boneModelName +"' url='\"" + boneModelName + ".x3d\"'/>");
                System.out.println("bone " + boneModelName + ".x3d source model:");
                System.out.println(boneModelX3D.toStringX3D());
            }
            // TODO check if changes are warranted
            else 
            {
                System.out.println("No change made to bone source model " + boneModelName + ".x3d due to geometry loading problem (probably too big), consider editing changes manually.");
            }
            
            return true; // processBone() successful
        }
        return false; // x3dLoaderDOM unsuccessful
	}


	/** 
	 * Provide a 
	 * <a href="https://dzone.com/articles/java-copy-shallow-vs-deep-in-which-you-will-swim" target="_blank">shallow copy</a>
	 * of the X3D model.
	 * @see <a href="https://www.web3d.org/specifications/java/javadoc/org/web3d/x3d/jsail/Core/X3D.html">X3D</a>
	 * @return c1 model
	 */
	public X3D getX3dModel()
	{	  
		return boneModelX3D;
	}
}
