
﻿// Only uncomment one of the following:
#define M_HOLOLENS_CODE_WITH_OR_WITHOUT_HEADSET
//#define M_VIVE_OR_DESKTOP // in this code, distinguish between desktop or headset with GC.isInDesktopMode

#if M_HOLOLENS_CODE_WITH_OR_WITHOUT_HEADSET
#if UNITY_EDITOR // for code running within the unity editor
#define M_HOLOLENS_CODE_WITHOUT_HEADSET // hololens codebase, but running in unity player, without headset (e.g. for debugging network stuff)
#elif WINDOWS_UWP // for code running on the Hololens headset
#define M_HOLOLENS_CODE_WITH_HEADSET
#endif
#endif

#if UNITY_EDITOR || WINDOWS_UWP
#define M_UNITY_API
#endif

using System;
using System.Collections.Generic;
using System.Linq;
//using System.Text;
//using System.Threading.Tasks;

//using System.IO;
//using System.Xml.Serialization;

#if M_HOLOLENS_CODE_WITH_HEADSET
using System.Threading.Tasks;
#endif


//namespace McGuffinsOfflineNetworkWorkshop {
    public class GeometryGlobals {
        // the width of 1 pixel in the text labels on the nodes
        public static float strokeWidth = 1.0f; // in millimeters

        public static float node_u_inStrokeWidths = 15;
        public static float node_v_inStrokeWidths = 7;
        public static float node_w_inStrokeWidths = 7;
        public static float edge_thickness_inStrokeWidths = 3;
        public static float thinEdge_thickness_inStrokeWidths = 0.5f;

        // used only for 2D labels which don't need margins to the left and right of the labels
        public static float label_u_inStrokeWidths = 11;

        public static float thinNode_v_inStrokeWidths = 3;

        public static float diameter = 150; // in millimeters: 150mm is just under 6 inches

        public static float pillar_thickness_inStrokeWidths = 6;
        public static float radius_of_zone_for_pillars_inMillimeters = 80.0f/2;
        public static float base_plate_radius_inMillimeters = 100.0f/2;
        public static float base_plate_thickness_inMillimeters = 4;
        public static float base_plate_lip_thickness_inMillimeters = 3;

        public static float base_holder_thickness_inMillimeters = 7;
        public static float base_holder_diameter_inMillimeters = 150;
        public static float base_holder_wing_diameter_inMillimeters = 50;

    }

    // This stores a box that is not necessarily axis-aligned.
    // It is designed to be usable by both clients interested in creating or updating Unity's 3D scene
    // (in which case the client will want to know the center of the cuboid, and will need the unit vectors for setting up orientation)
    // and clients interesting in exporting OBJ geometry.
    class Cuboid {
        public MVector3 center;

        // These are unit vectors aligned with the cuboid
        public MVector3 unit_u; // aligned with the length of the cuboid
        public MVector3 unit_v; // aligned with the height of the cuboid
        public MVector3 unit_w; // aligned with the thickness of the cuboid

        // These are the dimensions of the cuboid
        public float dim_u;
        public float dim_v;
        public float dim_w;
    }

    // A client must extend this class to be able to receive the results of the geometry generator.
    abstract class NetworkGeometryVisitor {
        abstract public void visitNode(Cuboid c, int nodeIndex);
        abstract public void visitLabelPixel(Cuboid c, int nodeIndex);
        abstract public void visitEdge(Cuboid c, int nodeIndex1, int nodeIndex2);
        virtual public void visitBaseElement(Cuboid c) { }
    }

    // The coordinates of nodes of the network are stored in a (local) *source* coordinate system, which is right-handed.
    // This class was created to enable identical-looking geometry to be generated for two different *target* coordinate systems:
    // Unity's coordinate system, which is left-handed and in units of meters,
    // and OBJ export for 3D printing in a right-handed system in millimeters.
    // Supporting these two coordinate systems might not sound like it would require an entire class to manage,
    // but this became necessary because I want the ability to generate 3D geometry for the letters that make up the labels on nodes
    // (to be able to 3D-print embossed letters),
    // and I also need to do non-trivial displacement of the endpoints of network edges to avoid occluding labels.
    class NetworkGeometryGenerator {

        public static readonly char[] charArray = new char[] { 'A', 'C', 'E', 'F', 'G', 'H', 'J', 'L', 'O', 'P', 'S', 'U' };
        public const int numChars = 12;
        public static string stringFromCharIndices(int char1, int char2) {
            return charArray[char1].ToString() + charArray[char2].ToString();
        }
        private static readonly string[][] charBitmaps = {
            new string[] {
                ".OO.",
                "O..O",
                "OOOO",
                "O..O",
                "O..O"
            },
            new string[] {
                "OOOO",
                "O...",
                "O...",
                "O...",
                "OOOO"
            },
            new string[] {
                "OOOO",
                "O...",
                "OOO.",
                "O...",
                "OOOO"
            },
            new string[] {
                "OOOO",
                "O...",
                "OOO.",
                "O...",
                "O..."
            },
            new string[] {
                "OOOO",
                "O...",
                "O.OO",
                "O..O",
                "OOOO"
            },
            new string[] {
                "O..O",
                "O..O",
                "OOOO",
                "O..O",
                "O..O"
            },
            new string[] {
                ".OOO",
                "..O.",
                "..O.",
                "..O.",
                "OOO."
            },
            new string[] {
                "O...",
                "O...",
                "O...",
                "O...",
                "OOOO"
            },
            new string[] {
                "OOOO",
                "O..O",
                "O..O",
                "O..O",
                "OOOO"
            },
            new string[] {
                "OOOO",
                "O..O",
                "OOOO",
                "O...",
                "O..."
            },
            new string[] {
                "OOOO",
                "O...",
                "OOOO",
                "...O",
                "OOOO"
            },
            new string[] {
                "O..O",
                "O..O",
                "O..O",
                "O..O",
                "OOOO"
            }
        };
        public const int charBitmap_w = 4; // in pixels
        public const int charBitmap_h = 5; // in pixels


        // These members store information about the target coordinate space, within which geometry will be generated.
        private MVector3 origin_target;
        private MVector3 unit_u_target;
        private MVector3 unit_v_target;
        private MVector3 unit_w_target;
        private MVector3 vec_u_target;
        private MVector3 vec_v_target;
        private MVector3 vec_w_target;
        private bool isTargetSpaceRightHanded;
        private float labelStrokeWidth;
        private float node_dim_u_target;
        private float node_dim_v_target;
        private float node_dim_w_target;
        private float edge_thickness_target;
        private float pillar_thickness_target;
        private float radius_of_zone_for_pillars_target;
        private float base_plate_radius_target;
        private float base_plate_thickness_target;
        private float base_plate_lip_thickness_target;
        private MAlignedBox3 bounds_source;
        private MVector3 origin_source;
        private MVector3 dim_source;

        // This routine specifies the target coordinate system,
        // to which the network's coordinate system will be transformed.
        // After calling this, the client can use computeNetworkGeometry()
        // to generate geometric information, and all such geometric info
        // passed to the visitor will be in the target coordinate system.
        //
        // The parameters below include a flag to indicate the handedness of the target system.
        // For either handedness, imagine text labels facing y+ out of the screen,
        // and x+ pointing to the right.
        // In a right-handed system (like the network's source space, or the system used for OBJ export for 3D printing)
        // we have z+ down.
        // In a left-handed system (like Unity),
        // we have z+ up.
        public void configureTargetSpace(
            MNetwork net,
            // These define the target space of the geometry that will be computed
            MVector3 origin_target,
            MVector3 unit_u_target, MVector3 unit_v_target, MVector3 unit_w_target, // unit vectors
            MVector3 vec_u_target, MVector3 vec_v_target, MVector3 vec_w_target, // *NOT* unit vectors
            bool isTargetSpaceRightHanded,

            float labelStrokeWidth, // in target space units; *IS IGNORED* if useCustomNodeSize is true
            bool makeEdgesThin = false,
            bool useCustomNodeSize = false,
            bool useNarrowNodes = false,
            bool useThinNodes = false,
            float labelSize_x_source = 0.0f,
            float labelSize_z_source = 0.0f
        ) {
            bounds_source = new MAlignedBox3();
            for (int i = 0; i < net.getNumNodes(); ++i) {
                MNode n = net.getNode(i);
                bounds_source.bound(n.getPosition());
            }
            origin_source = bounds_source.getMin();
            MVector3 bounds_source_size = bounds_source.getDiagonal();
            MLogger.Log(string.Format("bounds_source_size: {0},{1},{2}",bounds_source_size.x,bounds_source_size.y,bounds_source_size.z),1);
            dim_source = new MVector3(
                bounds_source_size.x > 0 ? bounds_source_size.x : 1.0f,
                bounds_source_size.y > 0 ? bounds_source_size.y : 1.0f,
                bounds_source_size.z > 0 ? bounds_source_size.z : 1.0f
            );

            this.origin_target = origin_target;
            this.unit_u_target = unit_u_target;
            this.unit_v_target = unit_v_target;
            this.unit_w_target = unit_w_target;
            this.vec_u_target = vec_u_target;
            this.vec_v_target = vec_v_target;
            this.vec_w_target = vec_w_target;
            this.isTargetSpaceRightHanded = isTargetSpaceRightHanded;
            this.labelStrokeWidth = useCustomNodeSize
                ? (labelSize_z_source/ GeometryGlobals.node_w_inStrokeWidths / dim_source.z * vec_w_target.magnitude())
                : labelStrokeWidth;

            node_dim_u_target = useNarrowNodes
                ? GeometryGlobals.label_u_inStrokeWidths * this.labelStrokeWidth
                : GeometryGlobals.node_u_inStrokeWidths * this.labelStrokeWidth;
            node_dim_v_target = useThinNodes
                ? GeometryGlobals.thinNode_v_inStrokeWidths * this.labelStrokeWidth
                : GeometryGlobals.node_v_inStrokeWidths * this.labelStrokeWidth;
            node_dim_w_target = GeometryGlobals.node_w_inStrokeWidths * this.labelStrokeWidth;
            edge_thickness_target = (makeEdgesThin?GeometryGlobals.thinEdge_thickness_inStrokeWidths:GeometryGlobals.edge_thickness_inStrokeWidths) * labelStrokeWidth;
            pillar_thickness_target = GeometryGlobals.pillar_thickness_inStrokeWidths * labelStrokeWidth;
            radius_of_zone_for_pillars_target // We want this in target_space_units. We calculate ...
                = GeometryGlobals.radius_of_zone_for_pillars_inMillimeters // in mm
                / GeometryGlobals.strokeWidth                              // in mm/strokeWidth
                * labelStrokeWidth;                                        // in target_space_units / strokeWidth
            base_plate_radius_target
                = GeometryGlobals.base_plate_radius_inMillimeters
                / GeometryGlobals.strokeWidth
                * labelStrokeWidth;
            base_plate_thickness_target
                = GeometryGlobals.base_plate_thickness_inMillimeters
                / GeometryGlobals.strokeWidth
                * labelStrokeWidth;
            base_plate_lip_thickness_target
                = GeometryGlobals.base_plate_lip_thickness_inMillimeters
                / GeometryGlobals.strokeWidth
                * labelStrokeWidth;
        }

        // Transforms a single given point from source space to target space.
        // Before calling this, be sure to call configureTargetSpace().
        public MVector3 convertSourceToTarget(MVector3 p_source) {
            // The source space is right-handed, with the origin in the left(x)-deepest(y)-upper(z) corner.
            MVector3 p = p_source - origin_source;

            // This normalizes the source coordinates to be within [0,1]x[0,1]x[0,1]
            p.x /= dim_source.x;
            p.y /= dim_source.y;
            p.z /= dim_source.z;

            if (!isTargetSpaceRightHanded) {
                // The target space is left-handed, with its origin in the left(x)-deepest(y)-lower(z) corner.
                return origin_target + p.x * vec_u_target + p.y * vec_v_target + (1.0f - p.z) * vec_w_target;
            }
            return origin_target + p.x * vec_u_target + p.y * vec_v_target + p.z * vec_w_target;
        }
        // only applies scale factors
        public float convertXLengthFromSourceToTarget(float l_source) {
            return l_source / dim_source.x * vec_u_target.magnitude();
        }
        public float convertZLengthFromSourceToTarget(float l_source) {
            return l_source / dim_source.z * vec_w_target.magnitude();
        }

        private void computeCharGeometry(
            NetworkGeometryVisitor visitor,
            MVector3 charCenter,
            int nodeIndex,
            int charIndex
        ) {
            MLogger.Assert(charBitmap_h == charBitmaps[charIndex].Length, "j8o0pz7uk0");
            for (int i = 0; i < charBitmap_h; ++i) {
                MLogger.Assert(charBitmap_w == charBitmaps[charIndex][i].Length, "6uwaugrs8s");
                for (int j = 0; j < charBitmap_w; ++j) {
                    if (charBitmaps[charIndex][i][j] == 'O') {
                        Cuboid c = new Cuboid();
                        c.center = charCenter
                            + ((j + 0.5f - charBitmap_w * 0.5f) * labelStrokeWidth * unit_u_target)
                            + (((isTargetSpaceRightHanded ? i : (charBitmap_h - 1 - i)) + 0.5f - charBitmap_h * 0.5f) * labelStrokeWidth * unit_w_target)
                        ;
                        c.unit_u = unit_u_target;
                        c.unit_v = unit_v_target;
                        c.unit_w = unit_w_target;
                        c.dim_u = labelStrokeWidth * 1.1f;
                        c.dim_v = labelStrokeWidth * 2.0f;
                        c.dim_w = labelStrokeWidth * 1.1f;
                        if (GC.renderFlattenedLabels) {
                            c.center += ( 0.5f * labelStrokeWidth ) * unit_v_target;
                            c.dim_v = labelStrokeWidth * 0.1f;
                        }
                        visitor.visitLabelPixel(c, nodeIndex);
                    }
                }
            }
        }

        // Before calling this, be sure to call configureTargetSpace().
        // The below routine traverses the network's nodes and edges,
        // and calls the client's visitor once for each of the following, in this order:
        // - node
        // - (optionally) pixel of a node's label
        // - edge segment (note that each edge could be made of multiple segments)
        // The routine computes and tells the visitor the size, position, and orientation of
        // a cuboid corresponding to the geometry of the element in question.
        // It is smart enough to reposition the endpoints of network edges to avoid occluding labels.
        // The visitor may then use this information to create or update or output geometric information.
        public void computeNetworkGeometry(
            MNetwork net, // whose nodes have positions in the source coordinate space
            NetworkGeometryVisitor visitor,
            bool visitNodes,
            bool visitEdges,
            bool visitLabelPixels, // whether a node's label's pixels should be traversed
            bool visitBaseElements = false, // whether we should generate a square base attached to the network by pillars
            bool visitEdgePolygonalSegments = false
        ) {
            MLogger.Assert(
                numChars == charArray.Length
                && numChars == charBitmaps.Length
                && charBitmap_h == charBitmaps[0].Length
                && charBitmap_w == charBitmaps[0][0].Length,
                "k38k5v1ato"
            );

            // iterate over network nodes
            for (int i = 0; i < net.getNumNodes(); ++i) {
                Cuboid c = new Cuboid();
                MNode node = net.getNode(i);
                c.center = convertSourceToTarget(node.getPosition());
                c.unit_u = unit_u_target;
                c.unit_v = unit_v_target;
                c.unit_w = unit_w_target;
                c.dim_u = node_dim_u_target;
                c.dim_v = node_dim_v_target;
                c.dim_w = node_dim_w_target;
                if ( visitNodes )
                    visitor.visitNode(c, i);

                if (visitLabelPixels) {
                    MVector3 offset_u = ((charBitmap_w + 1) * labelStrokeWidth / 2) * unit_u_target;
                    MVector3 offset_v = (node_dim_v_target / 2) * unit_v_target;
                    computeCharGeometry(visitor, c.center - offset_u + offset_v, i, node.label_char1);
                    computeCharGeometry(visitor, c.center + offset_u + offset_v, i, node.label_char2);
                }
            }

            // generate pillars and base-plate
            if ( visitBaseElements ) {
                // Each node could potentially have a pillar created connecting it to the base-plate.
                // We would like to have a minimum number of pillars in each quadrant of the base-plate, for stability.
                // We create 4 lists, one for each quadrant, to store candidate nodes.
                // In the source space, these quandrants correspond to (x-,z-), (x+,z-), (x-,z+), (x+,z+)
                // corresponding to the abbreviations xnzn, xpzn, xnzp, xpzp, respectively (n for negative, p for positive).
                const int xnzn = 0;
                const int xnzp = 1;
                const int xpzn = 2;
                const int xpzp = 3;
                List<int>[] indicesOfNodesWithPillars = new List<int>[4];
                indicesOfNodesWithPillars[xnzn] = new List<int>();
                indicesOfNodesWithPillars[xnzp] = new List<int>();
                indicesOfNodesWithPillars[xpzn] = new List<int>();
                indicesOfNodesWithPillars[xpzp] = new List<int>();
                MVector3 center_source = bounds_source.getCenter();
                MVector3 center_target = convertSourceToTarget(center_source);
                int indexOfNodeWithLowestY = -1;
                // Populate the lists of candidates with nodes that lie within the requisite radius
                for (int i = 0; i < net.getNumNodes(); ++i) {
                    MNode node = net.getNode(i);
                    MVector3 node_center_source = node.getPosition();
                    if (indexOfNodeWithLowestY<0 || node_center_source.y < net.getNode(indexOfNodeWithLowestY).getPosition().y)
                        indexOfNodeWithLowestY = i;
                    MVector3 d_source = node_center_source - center_source;
                    MVector3 node_center_target = convertSourceToTarget(node_center_source);
                    MVector3 d_target = node_center_target - center_target;
                    float radius = Math.Max(Math.Abs(d_target * unit_u_target), Math.Abs(d_target * unit_w_target));
                    if ( radius < radius_of_zone_for_pillars_target - pillar_thickness_target/2 ) {
                        int ii = (d_source.x>0?2:0) + (d_source.z > 0 ? 1 : 0);

                        // TODO_0 a hack to put all candidates in a single list :-(
                        // This is not ideal, but will do for now
                        ii = 0;

                        indicesOfNodesWithPillars[ii].Add(i);
                    }
                }
                // Now sort each of the lists of candidates by the y coordinate in the source space
                for (int ii = 0; ii < indicesOfNodesWithPillars.Length; ++ii) {
                    indicesOfNodesWithPillars[ii] = indicesOfNodesWithPillars[ii].OrderBy(i => (net.getNode(i).getPosition().y)).ToList();
                }
                // At this point, a clean and robust solution would iterate over the candidates
                // in each list and check if their pillars will intersect with any deeper (lower y_source)
                // nodes and eliminate those candidates.
                // However, as a quick and dirty solution, we will just assume that the first N candidates
                // in each list will not interfere with any other nodes.
                const int pillarsPerQuadrant = 3; // TODO_0 this is not ideal, but will do for now
                for (int ii = 0; ii < indicesOfNodesWithPillars.Length; ++ii) {
                    // delete all but the first 3 candidates in each list
                    if (indicesOfNodesWithPillars[ii].Count > pillarsPerQuadrant )
                        indicesOfNodesWithPillars[ii] = indicesOfNodesWithPillars[ii].GetRange(0,pillarsPerQuadrant);
                }
                // Compute the base-plate's geometry
                Cuboid basePlate = new Cuboid();
                basePlate.center = center_target - unit_v_target*(((center_target - convertSourceToTarget(net.getNode(indexOfNodeWithLowestY).getPosition()))*unit_v_target) + node_dim_v_target/2 + 1.1f*base_plate_lip_thickness_target + base_plate_thickness_target/2);
                basePlate.unit_u = unit_u_target;
                basePlate.unit_v = unit_v_target;
                basePlate.unit_w = unit_w_target;
                basePlate.dim_u = base_plate_radius_target * 2;
                basePlate.dim_v = base_plate_thickness_target;
                basePlate.dim_w = base_plate_radius_target * 2;

                visitor.visitBaseElement(basePlate);
                // Now generate a pillar for each candidate
                for (int ii = 0; ii < indicesOfNodesWithPillars.Length; ++ii) {
                    for ( int jj = 0; jj < indicesOfNodesWithPillars[ii].Count; ++jj ) {
                        int i = indicesOfNodesWithPillars[ii][jj];
                        MNode node = net.getNode(i);
                        MVector3 node_position_target = convertSourceToTarget(node.getPosition());
                        Cuboid pillar = new Cuboid();
                        pillar.unit_u = unit_u_target;
                        pillar.unit_v = unit_v_target;
                        pillar.unit_w = unit_w_target;
                        pillar.dim_u = pillar_thickness_target;
                        pillar.dim_v = (node_position_target - basePlate.center) * unit_v_target;
                        pillar.dim_w = pillar_thickness_target;
                        pillar.center = node_position_target - unit_v_target*(pillar.dim_v / 2);
                        visitor.visitBaseElement(pillar);
                    }
                }
            }

            // iterate over the network edges
            if (visitEdges) for (int nodeIndex1 = 0; nodeIndex1 < net.getNumNodes(); ++nodeIndex1) {
                MNode n1 = net.getNode(nodeIndex1);
                for (int j = 0; j < n1.degree(); ++j) {
                    MNode n2 = n1.getIthNeighbor(j);
                    int nodeIndex2 = net.getIndexOfNode(n2);
                    if (nodeIndex2 < nodeIndex1)
                        continue;
                    MEdge medge = net.edges[n1.indicesOfEdges[j]];

                    if (visitEdgePolygonalSegments && medge.polyline.Count>0) {

                        MVector2 p0_2D = medge.polyline[0];
                        MVector3 p0 = new MVector3(p0_2D.x, 0, p0_2D.y);

                        for ( int ii = 1; ii < medge.polyline.Count; ++ii ) {
                            MVector2 p1_2D = medge.polyline[ii];
                            MVector3 p1 = new MVector3(p1_2D.x, 0, p1_2D.y);

                            // convert coordinates to target space
                            MVector3 end1 = convertSourceToTarget(p0);
                            MVector3 end2 = convertSourceToTarget(p1);

                            MVector3 edge_vec_u = end2 - end1;
                            MVector3 edge_unit_u = edge_vec_u.normalized();
                            MVector3 edge_unit_v = edge_vec_u.choosePerpendicular().normalized();
                            MVector3 edge_unit_w = MVector3.cross(edge_unit_u, edge_unit_v).normalized();

                            Cuboid c = new Cuboid();
                            c.center = (end1 + end2) * 0.5f;
                            c.unit_u = edge_unit_u;
                            c.unit_v = edge_unit_v;
                            c.unit_w = edge_unit_w;
                            c.dim_u = edge_vec_u.magnitude();
                            c.dim_v = edge_thickness_target;
                            c.dim_w = edge_thickness_target;

                            visitor.visitEdge(c, nodeIndex1, nodeIndex2);

                            p0 = p1;
                        }
                    }
                    else {
                        // compute the endpoints of the edge
                        MVector3 end1_source = n1.getPosition();
                        MVector3 end2_source = n2.getPosition();

                        // Ensure that end1 is the deeper endpoint of the edge (along the y+ direction which text labels are facing)
                        if (end1_source.y >= end2_source.y) {
                            // swap
                            MVector3 tmp = end2_source;
                            end2_source = end1_source;
                            end1_source = tmp;
                        }

                        // convert coordinates to target space
                        MVector3 end1 = convertSourceToTarget(end1_source);
                        MVector3 end2 = convertSourceToTarget(end2_source);


                        // shift the endpoint end1 in the x+ or x- direction
                        end1 += (end2_source.x > end1_source.x ? 1 : -1) * (node_dim_u_target / 2 - edge_thickness_target / 2) * unit_u_target;

                        MVector3 edge_vec_u = end2 - end1;
                        MVector3 edge_unit_u = edge_vec_u.normalized();
                        MVector3 edge_unit_v = edge_vec_u.choosePerpendicular().normalized();
                        MVector3 edge_unit_w = MVector3.cross(edge_unit_u, edge_unit_v).normalized();

                        Cuboid c = new Cuboid();
                        c.center = (end1 + end2) * 0.5f;
                        c.unit_u = edge_unit_u;
                        c.unit_v = edge_unit_v;
                        c.unit_w = edge_unit_w;
                        c.dim_u = edge_vec_u.magnitude();
                        c.dim_v = edge_thickness_target;
                        c.dim_w = edge_thickness_target;

                        visitor.visitEdge(c, nodeIndex1, nodeIndex2);
                    }
                }
            }
        }
    }

    class ObjFace {
        public int[] vertexIndices = new int[4];
        public int normalIndex;
        public ObjFace(int v1, int v2, int v3, int v4, int n) {
            vertexIndices[0] = v1;
            vertexIndices[1] = v2;
            vertexIndices[2] = v3;
            vertexIndices[3] = v4;
            normalIndex = n;
        }
    }

    public class ObjExporter {

        private static List<MVector3> vertices = new List<MVector3>();
        private static List<MVector3> normals = new List<MVector3>();
        private static List<ObjFace> faces = new List<ObjFace>();

        class OBJExportingVisitor : NetworkGeometryVisitor {
            public void exportCuboid(Cuboid c) {
                int vN = vertices.Count;
                int vnN = normals.Count;
                int fN = faces.Count;
                MVector3 half_vec_u = c.dim_u / 2 * c.unit_u;
                MVector3 half_vec_v = c.dim_v / 2 * c.unit_v;
                MVector3 half_vec_w = c.dim_w / 2 * c.unit_w;
                vertices.Add(c.center + (-half_vec_u - half_vec_v - half_vec_w));
                vertices.Add(c.center + (half_vec_u - half_vec_v - half_vec_w));
                vertices.Add(c.center + (-half_vec_u + half_vec_v - half_vec_w));
                vertices.Add(c.center + (half_vec_u + half_vec_v - half_vec_w));
                vertices.Add(c.center + (-half_vec_u - half_vec_v + half_vec_w));
                vertices.Add(c.center + (half_vec_u - half_vec_v + half_vec_w));
                vertices.Add(c.center + (-half_vec_u + half_vec_v + half_vec_w));
                vertices.Add(c.center + (half_vec_u + half_vec_v + half_vec_w));
                /*
                 The number of vertices in OBJ is 1-based. Assume a right-handed system, with u+ right, v+ up, w+ out.
                      3   4
                    7   8
                      1   2
                    5   6
                */
                // These normals must be in the same order as the 6 faces.
                normals.Add(-c.unit_w); // back (facing in)
                normals.Add(c.unit_w); // front (facing out)
                normals.Add(c.unit_u); // right
                normals.Add(c.unit_v); // top
                normals.Add(-c.unit_u); // left
                normals.Add(-c.unit_v); // bottom
                                        // the indices here are 1-based, not 0-based, because 1-based indices are used in the OBJ format
                faces.Add(new ObjFace(vN + 1, vN + 3, vN + 4, vN + 2, vnN + 1)); // back (facing in)
                faces.Add(new ObjFace(vN + 5, vN + 6, vN + 8, vN + 7, vnN + 2)); // front (facing out)
                faces.Add(new ObjFace(vN + 6, vN + 2, vN + 4, vN + 8, vnN + 3)); // right
                faces.Add(new ObjFace(vN + 7, vN + 8, vN + 4, vN + 3, vnN + 4)); // top
                faces.Add(new ObjFace(vN + 5, vN + 7, vN + 3, vN + 1, vnN + 5)); // left
                faces.Add(new ObjFace(vN + 1, vN + 2, vN + 6, vN + 5, vnN + 6)); // bottom
            }
            public override void visitNode(Cuboid c, int nodeIndex) { exportCuboid(c); }
            public override void visitLabelPixel(Cuboid c, int nodeIndex) { exportCuboid(c); }
            public override void visitEdge(Cuboid c, int nodeIndex1, int nodeIndex2) { exportCuboid(c); }
            public override void visitBaseElement(Cuboid c) { exportCuboid(c); }
        }
        private static string generateString() {
            string returnValue = "";
            for (int i = 0; i < vertices.Count; ++i) {
                MVector3 v = vertices[i];
                returnValue += string.Format("v {0:####.00} {1:####.00} {2:####.00}\n", v.x, v.y, v.z);
            }
            returnValue += "\n\n\n";
            for (int i = 0; i < normals.Count; ++i) {
                MVector3 vn = normals[i];
                returnValue += string.Format("vn {0:####.00} {1:####.00} {2:####.00}\n", vn.x, vn.y, vn.z);
            }
            returnValue += "\n\n\n";
            for (int i = 0; i < faces.Count; ++i) {
                ObjFace f = faces[i];
                returnValue += string.Format("f {0}//{4} {1}//{4} {2}//{4} {3}//{4}\n", f.vertexIndices[0], f.vertexIndices[1], f.vertexIndices[2], f.vertexIndices[3], f.normalIndex);
            }
            return returnValue;
        }
        public static string exportOBJString(MNetwork net,bool withEmbossedLabels,bool withBaseplate, bool visitEdgePolygonalSegments=false) {
            vertices.Clear();
            normals.Clear();
            faces.Clear();

            // (DUPLICATE_CODE) The below calculation is nearly duplicated in mainScript.cs
            float labelStrokeWidth = GeometryGlobals.strokeWidth; // in millimeters
            float dim_target = GeometryGlobals.diameter - GeometryGlobals.node_u_inStrokeWidths * labelStrokeWidth; // in millimeters (150mm is just under 6 inches)
            NetworkGeometryGenerator generator = new NetworkGeometryGenerator();
            generator.configureTargetSpace(net, new MVector3(0, 0, 0),
                new MVector3(1, 0, 0), new MVector3(0, 1, 0), new MVector3(0, 0, 1),
                new MVector3(dim_target, 0, 0), new MVector3(0, dim_target, 0), new MVector3(0, 0, dim_target),
                true, labelStrokeWidth
            );
            OBJExportingVisitor visitor = new OBJExportingVisitor();
            generator.computeNetworkGeometry(net, visitor, true, true, withEmbossedLabels, withBaseplate, visitEdgePolygonalSegments);

            return generateString();
        }
        public static string exportOBJStringForBaseHolder() {
            vertices.Clear();
            normals.Clear();
            faces.Clear();
            OBJExportingVisitor v = new OBJExportingVisitor();

            MVector3 unit_x = new MVector3(1, 0, 0);
            MVector3 unit_y = new MVector3(0, 1, 0);
            MVector3 unit_z = new MVector3(0, 0, 1);

            Cuboid c = new Cuboid();
            c.unit_u = unit_x;
            c.unit_v = unit_y;
            c.unit_w = unit_z;
            c.center = new MVector3(0,0,0);
            c.dim_u = GeometryGlobals.base_holder_diameter_inMillimeters;
            c.dim_v = GeometryGlobals.base_holder_thickness_inMillimeters;
            c.dim_w = GeometryGlobals.base_holder_diameter_inMillimeters;
            v.exportCuboid(c);

            // horizontal lip overhang
            float a = (GeometryGlobals.base_plate_radius_inMillimeters+1 - (GeometryGlobals.radius_of_zone_for_pillars_inMillimeters+1));
            float a_small = a * 0.5f;
            // horizontal thickness of thing holding the lip
            float b = 2 * a;

            Cuboid lipBase1 = new Cuboid();
            lipBase1.unit_u = unit_x;
            lipBase1.unit_v = unit_y;
            lipBase1.unit_w = unit_z;
            lipBase1.dim_u = b;
            lipBase1.dim_v = (GeometryGlobals.base_holder_thickness_inMillimeters / 2 + GeometryGlobals.base_plate_thickness_inMillimeters + GeometryGlobals.base_plate_lip_thickness_inMillimeters);
            lipBase1.dim_w = GeometryGlobals.base_plate_radius_inMillimeters * 2;
            lipBase1.center = c.center - unit_x * ((GeometryGlobals.base_plate_radius_inMillimeters + 1) + b / 2)
                + unit_y * (lipBase1.dim_v/2);
            v.exportCuboid(lipBase1);

            Cuboid lipBase2 = new Cuboid();
            lipBase2.unit_u = unit_x;
            lipBase2.unit_v = unit_y;
            lipBase2.unit_w = unit_z;
            lipBase2.dim_u = lipBase1.dim_u;
            lipBase2.dim_v = lipBase1.dim_v;
            lipBase2.dim_w = lipBase1.dim_w;
            lipBase2.center = c.center + unit_x * ((GeometryGlobals.base_plate_radius_inMillimeters + 1) + b / 2)
                + unit_y * (lipBase2.dim_v / 2);
            v.exportCuboid(lipBase2);

            Cuboid base3 = new Cuboid();
            base3.unit_u = unit_x;
            base3.unit_v = unit_y;
            base3.unit_w = unit_z;
            base3.dim_u = 10;
            base3.dim_v = 2 * GeometryGlobals.base_plate_thickness_inMillimeters;
            base3.dim_w = 6;
            base3.center = c.center + unit_z * ((GeometryGlobals.base_plate_radius_inMillimeters + 1) + base3.dim_w / 2)
                + unit_y * (GeometryGlobals.base_holder_thickness_inMillimeters / 2);
            v.exportCuboid(base3);

            Cuboid lip1 = new Cuboid();
            lip1.unit_u = unit_x;
            lip1.unit_v = unit_y;
            lip1.unit_w = unit_z;
            lip1.dim_u = a_small + b;
            lip1.dim_v = GeometryGlobals.base_plate_lip_thickness_inMillimeters;
            lip1.dim_w = lipBase1.dim_w;
            lip1.center = c.center - unit_x * ((GeometryGlobals.radius_of_zone_for_pillars_inMillimeters + 1) + (a+b) / 2 + (a-a_small)/2)
                + unit_y * (c.dim_v/2 + GeometryGlobals.base_plate_thickness_inMillimeters + lip1.dim_v/2);
            v.exportCuboid(lip1);

            Cuboid lip2 = new Cuboid();
            lip2.unit_u = unit_x;
            lip2.unit_v = unit_y;
            lip2.unit_w = unit_z;
            lip2.dim_u = lip1.dim_u;
            lip2.dim_v = lip1.dim_v;
            lip2.dim_w = lip1.dim_w;
            lip2.center = c.center + unit_x * ((GeometryGlobals.radius_of_zone_for_pillars_inMillimeters + 1) + (a + b) / 2 + (a-a_small)/2)
                + unit_y * (c.dim_v / 2 + GeometryGlobals.base_plate_thickness_inMillimeters + lip1.dim_v / 2);
            v.exportCuboid(lip2);

            // wings
            float t = GeometryGlobals.base_holder_thickness_inMillimeters;
            float w = GeometryGlobals.base_holder_wing_diameter_inMillimeters;

            // wing facing -x
            MVector3 unit_x2 = new MVector3(1, 1, 0).normalized();
            MVector3 unit_y2 = new MVector3(-1, 1, 0).normalized();
            Cuboid wing = new Cuboid();
            wing.unit_u = unit_x2;
            wing.unit_v = unit_y2;
            wing.unit_w = unit_z;
            wing.dim_u = w;
            wing.dim_v = t;
            wing.dim_w = w;
            wing.center = c.center + unit_y*(t/2) - unit_x*(GeometryGlobals.base_holder_diameter_inMillimeters/2) - unit_x2*(w/2) - unit_y2*(t/2);
            v.exportCuboid(wing);

            // wing facing +x
            unit_x2 = new MVector3(1, -1, 0).normalized();
            unit_y2 = new MVector3(1, 1, 0).normalized();
            wing.unit_u = unit_x2;
            wing.unit_v = unit_y2;
            wing.unit_w = unit_z;
            wing.center = c.center + unit_y * (t / 2) + unit_x * (GeometryGlobals.base_holder_diameter_inMillimeters / 2) + unit_x2 * (w / 2) - unit_y2 * (t / 2);
            v.exportCuboid(wing);

            // wing facing +z
            unit_y2 = new MVector3(0, 1, 1).normalized();
            MVector3 unit_z2 = new MVector3(0,-1,1).normalized();
            wing.unit_u = unit_x;
            wing.unit_v = unit_y2;
            wing.unit_w = unit_z2;
            wing.center = c.center + unit_y * (t / 2) + unit_z * (GeometryGlobals.base_holder_diameter_inMillimeters / 2) + unit_z2 * (w / 2) - unit_y2 * (t / 2);
            v.exportCuboid(wing);

            // wing facing -z
            unit_y2 = new MVector3(0, 1, -1).normalized();
            unit_z2 = new MVector3(0, 1, 1).normalized();
            wing.unit_u = unit_x;
            wing.unit_v = unit_y2;
            wing.unit_w = unit_z2;
            wing.center = c.center + unit_y * (t / 2) - unit_z * (GeometryGlobals.base_holder_diameter_inMillimeters / 2) - unit_z2 * (w / 2) - unit_y2 * (t / 2);
            v.exportCuboid(wing);

            // handle
            Cuboid handle = new Cuboid();
            handle.unit_u = unit_x;
            handle.unit_v = unit_y;
            handle.unit_w = unit_z;
            handle.dim_u = 15;
            handle.dim_v = 60;
            handle.dim_w = 15;
            handle.center = new MVector3(0,-handle.dim_v/2, 0);
            v.exportCuboid(handle);

            return generateString();
        }
    }


    class MNetworkUtil {

        // Returns a regular ring lattice (or something similar if degree is odd).
        // We require degree at least 2, and numNodes at least (degree+1).
        // The network will have approximately E = (numNodes * degree / 2) edges
        // (or exactly if degree is even).
        public static MNetwork generateRegularRingLattice(int numNodes, int degree) {
            if (degree < 2 || numNodes <= degree)
                return null;

            MNetwork net = new MNetwork();
            for (int i = 0; i < numNodes; ++i)
                net.addNode(new MNode());
            int delta = degree / 2;  // delta is floor(degree/2)

            // We start by generating a regular ring lattice by attaching edges between
            // each node n_i and the nodes n_{i+1},...,n_{i+delta} and wrapping all the way around.
            // After doing this, each node n_i will have degree 2*delta
            // and have neighbors n_{i-delta},...,n_{i-1},n_{i+1},...,n_{i+delta}
            //
            for (int i = 0; i < numNodes; ++i) {
                MNode n_i = net.getNode(i);
                for (int i2 = i + 1; i2 <= i + delta; ++i2) {
                    int j = i2 % numNodes;
                    MNode n_j = net.getNode(j);
                    MLogger.Assert(!net.areNodesAdjacent(n_i, n_i), "ta04gjhqa5");
                    net.addEdge(n_i, n_j);
                }
                MLogger.Assert(i < delta || n_i.degree() == 2 * delta, "avv09nmcp8");
            }

            MLogger.Assert(net.getNumNodes() == numNodes, "de61qicr00");
            MLogger.Assert(net.getNumEdges() == numNodes * delta, "wyvm42o9bn");

            // If degree is odd, we are not finished creating the initial network.
            // We must still add some extra edges.
            //
            if (degree % 2 == 1) {
                for (int i = 0; i < numNodes; ++i) {
                    MNode n_i = net.getNode(i);
                    MLogger.Assert(n_i.degree() == degree - 1 || n_i.degree() == degree, "c19qfx0ee0");
                    if (n_i.degree() < degree) {
                        MNode n_j = net.getNode((i + delta + 1) % numNodes);
                        if (n_j.degree() < degree)
                            net.addEdge(n_i, n_j);
                    }
                }
            }
            return net;
        }

        // Generates something similar to a scale-free graph.
        // Returns a network with approximately E = (numNodes * meanDegree / 2) edges
        // (or exactly if meanDegree is even).
        // The algorithm first constructs a regular ring lattice (or something similar if meanDegree is odd).
        // Then it applies the algorithm in section 2 of
        // https://scholar.google.ca/scholar?q=eppstein+wang+steady+state+model+graph+power+laws
        public static MNetwork generateEppsteinWangPowerLawNetwork(
            int numNodes, int meanDegree, System.Random random
        ) {
            if (numNodes < 2 || meanDegree <= 0)
                return null;
            if (meanDegree >= numNodes)
                meanDegree = numNodes - 1;

            MNetwork net = generateRegularRingLattice(numNodes, meanDegree);

            int sumOfDegrees = 0;
            for (int i = 0; i < net.getNumNodes(); ++i)
                sumOfDegrees += net.getNode(i).degree();
            MLogger.Assert(sumOfDegrees == 2*net.getNumEdges(), "bl8c6w9158");

            for (int i = 0; i < 200 * numNodes; ++i) {
                // pick a random node v with positive degree
                int v_i;
                MNode v;
                do {
                    v_i = random.Next(0, net.getNumNodes());
                    v = net.getNode(v_i);
                } while (v.degree() == 0);

                // choose a random neighbor u of v
                int ii = random.Next(0, v.degree());
                int u_i = v.getIndexOfIthNeighbor(ii);
                MNode u = net.getNode(u_i);

                MLogger.Assert(net.areNodesAdjacent(u_i, v_i),"c2vrq3nlwe");

                // pick a random node x
                int x_i = random.Next(0, net.getNumNodes());
                MNode x = net.getNode(x_i);

                // pick a random node y with probability proportional to its degree
                ii = random.Next(0, sumOfDegrees);
                int partialSum = 0;
                int y_i = 0;
                MNode y;
                while (true) {
                    y = net.getNode(y_i);
                    if (partialSum + y.degree() <= ii) { // skip to next node
                        partialSum += y.degree();
                        y_i++;
                    }
                    else break;
                    MLogger.Assert(y_i < net.getNumNodes(), "ayecat6aem");
                }
                MLogger.Assert(y_i < net.getNumNodes(), "87u11et5q0");

                // If x,y are not the same node, and x,y are not adjacent,
                // then remove edge (u,v) and add edge (x,y)
                // (To avoid ending up with degree-zero nodes,
                // we add an extra condition: only rewire if u,v each have degree greater than 1
                if (x_i != y_i && !net.areNodesAdjacent(x_i,y_i)
                    && u.degree() > 1 && v.degree() > 1
                ) {
                    // We don't currently have support for removing edges (because this would require
                    // updating the indices of other edges stored by nodes),
                    // and this would be a slow operation anyway,
                    // so as a hack we'll just reuse the same
                    // edge and "rewire" the nodes involved.

                    // Find index of edge involved
                    if (u.degree() > v.degree()) {
                        // swap u and v
                        int tmp = u_i;
                        u_i = v_i;
                        v_i = tmp;
                        MNode tmp_n = u;
                        u = v;
                        v = tmp_n;
                    }
                    int e_i = u.indicesOfEdges[0];
                    MEdge e = net.edges[e_i];
                    foreach ( int jj in u.indicesOfEdges ) {
                        e_i = jj;
                        e = net.edges[e_i];
                        if (e.getIndexOfOtherNode(u_i) == v_i) break;
                    }
                    MLogger.Assert(e.getIndexOfOtherNode(u_i) == v_i && e.getIndexOfOtherNode(v_i) == u_i,"tu9syyy6xj");

                    // Remove the index of the edge from nodes u and v
                    u.indicesOfEdges.Remove(e_i);
                    v.indicesOfEdges.Remove(e_i);
                    // Add the index of the edge to nodes x and y
                    x.indicesOfEdges.Add(e_i);
                    y.indicesOfEdges.Add(e_i);
                    // Update the edge
                    e.indexOfNode1 = x_i;
                    e.indexOfNode2 = y_i;
                }
            }
            return net;
        }

        // This returns a network with approximately E = (numNodes * meanDegree / 2) edges
        // (or exactly if meanDegree is even).
        // The algorithm first constructs a regular ring lattice (or something similar if meanDegree is odd).
        // Each edge is then randomly rewired with probability beta.
        // A beta of 1 yields an Erdős–Rényi random graph.
        // A beta of 0 yields the unmodified lattice.
        // The (unpublished?) paper
        //   https://scholar.google.ca/scholar?q=dawson+mcgrenere+munzner+moffatt+findlater+ephemeral+paths+gradual
        // used numNodes=300, meanDegree=4 (hence 600 edges), beta=0.1
        // and cites
        //   https://scholar.google.ca/scholar?q=Auber+Chiricota+Jourdan+Melancon+Multiscale+visualization+of+small+world+networks
        //   https://scholar.google.ca/scholar?q=Watts+Strogatz+Collective+dynamics+of+small+world+networks
        //   https://scholar.google.ca/scholar?q=melancon+%22just+how+dense+are+dense+graphs+in+the+real+world%22
        //
        public static MNetwork generateWattsStrogatzSmallWorldNetwork(
            int numNodes, int meanDegree, float beta, System.Random random
        ) {
            if (numNodes < 2 || meanDegree <= 0)
                return null;
            if (meanDegree >= numNodes)
                meanDegree = numNodes - 1;

            MNetwork net = generateRegularRingLattice(numNodes, meanDegree);

            if (beta < 0) beta = 0;
            else if (beta > 1) beta = 1;

            // Now we iterate over all edges, and for each edge,
            // decide with probability beta to rewire it.
            // To avoid encountering rewired edges more than once
            // and having to keep track of which edges had already been processed,
            // we make a whole new network and copy all edges into it.
            MNetwork net2 = new MNetwork();
            for (int i = 0; i < numNodes; ++i)
                net2.addNode(new MNode());
            // iterate over all edges in the original network
            for (int n1_index = 0; n1_index < numNodes; ++n1_index) {
                MNode n1 = net.getNode(n1_index);
                for (int j = 0; j < n1.degree(); ++j) {
                    MNode n2 = n1.getIthNeighbor(j);
                    int n2_index = net.getIndexOfNode(n2);
                    if (n2_index < n1_index)
                        // don't process each edge twice
                        continue;
                    // decide with probability beta to rewire
                    if ((float)random.NextDouble() < beta) {
                        int n2_index_new = indexOfRandomlyChosenNode(net,n1_index, random);
                        if (!net.areNodesAdjacent(n1, net.getNode(n2_index_new))) {
                            n2_index = n2_index_new;
                        }
                    }
                    // add the edge to the new network
                    net2.addEdge(n1_index, n2_index);
                }
            }
            return net2;
        }
        private static int indexOfRandomlyChosenNode(
            MNetwork net,
            int indexOfNodeToNotChoose, // -1 if any node may be chosen
            System.Random random
        ) {
            if (net.getNumNodes() == 0) return -1;
            if (net.getNumNodes() == 1 && indexOfNodeToNotChoose == 0) return -1;
            if (indexOfNodeToNotChoose < 0) {
                return random.Next(0, net.getNumNodes());
            }
            int index = random.Next(0, net.getNumNodes() - 1);
            if (index >= indexOfNodeToNotChoose) {
                ++index;
                MLogger.Assert(index < net.getNumNodes(), "29h9oly002");
            }
            return index;
        }

        // Randomizes positions to be within [0,1]x[0,1]x[0,1]
        public static void randomizePositionsOfNodes( MNetwork net, System.Random r ) {
            for (int i = 0; i < net.nodes.Count; ++i) {
                MNode n = net.nodes[i];
                n.x = (float)r.NextDouble(); // returns a value in [0,1)
                n.y = (float)r.NextDouble(); // returns a value in [0,1)
                n.z = (float)r.NextDouble(); // returns a value in [0,1)
            }
        }

        // computes the "weight" associated with the desired distance between two nodes
        private static float distanceWeight(MNetwork net, int i, int j) {
            float d = net.getShortestPathMatrix().get(i, j);
            return (d <= 0) ? 0 : (1.0f / (d * d));
        }

        private static float embeddedDistance(MNetwork net, MNode n1, MNode n2, bool ignoreY = false) {
            float dx = n1.x - n2.x;
            float dz = n1.z - n2.z;

            if (net.getLayoutConstrainedTo2D() || ignoreY) {
                return (float)Math.Sqrt(dx * dx + dz * dz);
            }
            float dy = n1.y - n2.y;
            return (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
        }

        // This implements equation (12) in Gansner, Koren, North, "Graph Drawing by Stress Majorization"
        //    http://scholar.google.ca/scholar?q=Gansner+Koren+North+%22Graph+Drawing+by+Stress+Majorization%22
        //    The d_{i,j} values in that equation are retrieved in the code from shortestPathMatrix.
        //    The w_{i,j} values in that equation are computed in the code by distanceWeight().
        //    The ||X_i - X_j|| in the equation are computed in the code by embeddedDistance().
        //
        // This method returns the maximum distance that any node moved along any single axis.
        // The caller can decide to stop iterating when this distance gets small enough.
        //
        public static float simulateOneStepOfStressMajorizationLayout(MNetwork net, bool updateXZ, bool updateY, bool ignoreY) {
            if (!updateXZ && !updateY) return 0;
            if (updateXZ && updateY && ignoreY )
                // empirical tests show that, under these conditions, the node positions can diverge in the y direction
                return 0;
            //if (isShortestPathMatrixDirty)
            //    computeShortestPathMatrix();
            int N = net.nodes.Count;
            float maximumDistanceMoved = 0;
            for (int i = 0; i < N; ++i) {
                MNode n_i = net.getNode(i);

                float total_w_i = 0;
                for (int j = 0; j < N; ++j) {
                    if (j != i) {
                        float w = distanceWeight(net, i, j);
                        total_w_i += w;
                    }
                }
                float new_x = 0, new_y = 0, new_z = 0;
                for (int j = 0; j < N; ++j) {
                    if (j != i) {
                        MNode n_j = net.getNode( j );
                        float ed = embeddedDistance(net, n_i, n_j, ignoreY);
                        float inverse_of_ed = (ed == 0) ? 0 : (1.0f / ed);
                        float d = net.getShortestPathMatrix().get(i, j);
                        float d_times_inv_of_ed = d * inverse_of_ed;
                        float w = distanceWeight(net, i, j);

                        new_x += w * (n_j.x + (n_i.x - n_j.x) * d_times_inv_of_ed);
                        if (!net.getLayoutConstrainedTo2D() && updateY)
                            new_y += w * (n_j.y + (n_i.y - n_j.y) * d_times_inv_of_ed);

                        new_z += w * (n_j.z + (n_i.z - n_j.z) * d_times_inv_of_ed);
                    }
                }
                new_x /= total_w_i;
                new_y /= total_w_i;
                new_z /= total_w_i;

                // check how far this node will be moved
                float dx = updateXZ ? Math.Abs(n_i.x - new_x) : 0;
                float dy = updateY  ? Math.Abs(n_i.y - new_y) : 0;
                float dz = updateXZ ? Math.Abs(n_i.z - new_z) : 0;
                // Find the max of dx, dy, dz.
                // This is not Euclidean distance, but is less expensive to compute, and is close enough.
                float distanceMoved = dx > dy ? dx : dy;
                if (dz > distanceMoved) distanceMoved = dz;
                // Update maximum distance
                if (distanceMoved > maximumDistanceMoved) maximumDistanceMoved = distanceMoved;

                // update position of node
                if (updateXZ) {
                    n_i.x = new_x;
                    n_i.z = new_z;
                }
                if (!net.getLayoutConstrainedTo2D() && updateY) {
                    n_i.y = new_y;
                }
            }
            //MyLogger.Log( "maxDistanceMoved==" + maximumDistanceMoved ); // for debugging
            return maximumDistanceMoved;
        }

        // http://blog.danskingdom.com/saving-and-loading-a-c-objects-data-to-an-xml-json-or-binary-file/
        public static void WriteNetToXmlFile(string fileName, MNetwork net) {

            MUtil.WriteXml<MNetwork>(fileName,net);
            /*
            string folder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            MyLogger.Log("Folder: " + folder);
            string filePath = Path.Combine(folder, fileName);
            TextWriter tw = null;
            try {
                var serializer = new XmlSerializer(typeof(MNetwork));
                tw = new StreamWriter(filePath, false);
                serializer.Serialize(tw, net);
            }
            finally {
                if (tw != null)
                    tw.Close();
            }
            */
        }
#if M_HOLOLENS_CODE_WITH_HEADSET
        async public static Task<MNetwork> ReadNetFromXmlFile(string fileName) {
            MNetwork network = await MUtil.ReadXml<MNetwork>(fileName);
#else
        public static MNetwork ReadNetFromXmlFile(string fileName) {
            MNetwork network = MUtil.ReadXml<MNetwork>(fileName);
#endif
            /*
            string folder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            MyLogger.Log("Folder: " + folder);
            string filePath = Path.Combine(folder, fileName);
            TextReader tr = null;
            try {
                var serializer = new XmlSerializer(typeof(MNetwork));
                tr = new StreamReader(filePath);
                return (MNetwork)serializer.Deserialize(tr);
            }
            finally {
                if (tr != null)
                    tr.Close();
            }
            */
            //MLogger.Log("num nodes after loading: "+network.getNumNodes());
            for ( int i=0; i < network.getNumNodes(); ++i ) {
                MNode n = network.getNode(i);
                n.setNetwork(network);
            }
            return network;
        }

        public static MNetwork ConstructNetFromXmlString(string input) {
            MNetwork network = MUtil.DeserializeXmlStringToObject<MNetwork>(input);

            for ( int i=0; i < network.getNumNodes(); ++i ) {
                MNode n = network.getNode(i);
                n.setNetwork(network);
            }
            return network;
        }

    } // class MNetworkUtil
//}

