library GeomUtils {
    /// misc constants
    int256 constant MIN_INT = type(int256).min;
    int256 constant MAX_INT = type(int256).max;

    /// constants for doing trig
    int256 constant PI = 3141592653589793238; // pi as an 18 decimal value (wad)

    /// parameters that control geometry creation
    struct GeomSpec {
        string name;
        int256 id;
        int256 forceInitialSize;
        uint256 maxPrisms;
        int256 minTriRad;
        int256 maxTriRad;
        bool varySize;
        int256 depthMultiplier;
        bool isSymmetricX;
        bool isSymmetricY;
        int256 probVertOpp;
        int256 probAdjRec;
        int256 probVertOppRec;
    }

    /// variables uses when creating the initial 2d triangles
    struct TriVars {
        uint256 nextTriIdx;
        int256[3][3][] tris;
        int256[3][3] tri;
        int256 zBackRef;
        int256 zFrontRef;
        int256[] zFronts;
        int256[] zBacks;
        bool recursiveAttempt;
    }

    /// variables used when creating 3d prisms
    struct GeomVars {
        int256 rotX;
        int256 rotY;
        int256 rotZ;
        int256[3][2] extents;
        int256[3] center;
        int256 width;
        int256 height;
        int256 extent;
        int256 scaleNum;
        uint256[] hltPrismIdx;
        int256[3][3][] trisBack;
        int256[3][3][] trisFront;
        uint256 nPrisms;
    }

    /** @dev generate parameters that will control how the geometry is built */
    function generateSpec(bytes32 tokenHash)
        external
        view
        returns (GeomSpec memory spec)
    {
        //  'randomly' select 1 of possible geometry specifications
        uint256 specId = getSpecId(
            tokenHash,
            [
                [int256(0), 1000],
                [int256(1000), 3000],
                [int256(3000), 3500],
                [int256(3500), 4500],
                [int256(4500), 5000],
                [int256(5000), 6000],
                [int256(6000), 8000]
            ]
        );

        bool isSymmetricX = GeomUtils.randN(tokenHash, "symmX", 0, 2) > 0;
        bool isSymmetricY = GeomUtils.randN(tokenHash, "symmY", 0, 2) > 0;

        int256 defaultDepthMultiplier = randN(tokenHash, "depthMult", 80, 120);
        int256 defaultMinTriRad = 4800;
        int256 defaultMaxTriRad = defaultMinTriRad * 3;
        uint256 defaultMaxPrisms = uint256(
            randN(tokenHash, "maxPrisms", 8, 16)
        );

        if (specId == 0) {
            /// all vertically opposite
            spec = GeomSpec({
                id: 0,
                name: "Verticalized",
                forceInitialSize: (defaultMinTriRad * 5) / 2,
                maxPrisms: defaultMaxPrisms,
                minTriRad: defaultMinTriRad,
                maxTriRad: defaultMaxTriRad,
                varySize: true,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 100,
                probVertOppRec: 100,
                probAdjRec: 0,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else if (specId == 1) {
            /// fully adjacent
            spec = GeomSpec({
                id: 1,
                name: "Adjoint",
                forceInitialSize: (defaultMinTriRad * 5) / 2,
                maxPrisms: defaultMaxPrisms,
                minTriRad: defaultMinTriRad,
                maxTriRad: defaultMaxTriRad,
                varySize: true,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 0,
                probVertOppRec: 0,
                probAdjRec: 100,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else if (specId == 2) {
            /// few but big
            spec = GeomSpec({
                id: 2,
                name: "Cetacean",
                forceInitialSize: 0,
                maxPrisms: 8,
                minTriRad: defaultMinTriRad * 3,
                maxTriRad: defaultMinTriRad * 4,
                varySize: true,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 50,
                probVertOppRec: 50,
                probAdjRec: 50,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else if (specId == 3) {
            /// lots but small
            spec = GeomSpec({
                id: 3,
                name: "Swarm",
                forceInitialSize: 0,
                maxPrisms: 16,
                minTriRad: defaultMinTriRad,
                maxTriRad: defaultMinTriRad * 2,
                varySize: true,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 50,
                probVertOppRec: 0,
                probAdjRec: 0,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else if (specId == 4) {
            /// all same size
            spec = GeomSpec({
                id: 4,
                name: "Isomorphic",
                forceInitialSize: 0,
                maxPrisms: defaultMaxPrisms,
                minTriRad: defaultMinTriRad,
                maxTriRad: defaultMaxTriRad,
                varySize: false,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 50,
                probVertOppRec: 50,
                probAdjRec: 50,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else if (specId == 5) {
            /// trains
            spec = GeomSpec({
                id: 5,
                name: "Extruded",
                forceInitialSize: 0,
                maxPrisms: 10,
                minTriRad: defaultMinTriRad,
                maxTriRad: defaultMaxTriRad,
                varySize: true,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 50,
                probVertOppRec: 50,
                probAdjRec: 50,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else if (specId == 6) {
            /// flatpack
            spec = GeomSpec({
                id: 6,
                name: "Uniform",
                forceInitialSize: 0,
                maxPrisms: 12,
                minTriRad: defaultMinTriRad,
                maxTriRad: defaultMaxTriRad,
                varySize: true,
                depthMultiplier: defaultDepthMultiplier,
                probVertOpp: 50,
                probVertOppRec: 50,
                probAdjRec: 50,
                isSymmetricX: isSymmetricX,
                isSymmetricY: isSymmetricY
            });
        } else {
            revert("invalid specId");
        }
    }

    /** @dev make triangles to the side of a reference triangle */
    function makeAdjacentTriangles(
        bytes32 tokenHash,
        uint256 attemptNum,
        uint256 refIdx,
        TriVars memory triVars,
        GeomSpec memory geomSpec,
        int256 overrideSideIdx,
        int256 overrideScale,
        int256 depth
    ) public view returns (TriVars memory) {
        /// get the side index (0, 1 or 2)
        int256 sideIdx;
        if (overrideSideIdx == -1) {
            sideIdx = randN(
                tokenHash,
                string(abi.encodePacked("sideIdx", attemptNum, depth)),
                0,
                2
            );
        } else {
            sideIdx = overrideSideIdx;
        }

        /// get the scale
        /// this value is scaled up by 1e3 (desired range is 0.333 to 0.8)
        /// the scale will be divided out when used
        int256 scale;
        if (geomSpec.varySize) {
            if (overrideScale == -1) {
                scale = randN(
                    tokenHash,
                    string(abi.encodePacked("scaleAdj", attemptNum, depth)),
                    333,
                    800
                );
            } else {
                scale = overrideScale;
            }
        } else {
            scale = 1e3;
        }

        /// make a new triangle
        int256[3][3] memory newTri = makeTriAdjacent(
            tokenHash,
            geomSpec,
            attemptNum,
            triVars.tris[refIdx],
            sideIdx,
            scale,
            depth
        );

        /// set the zbackref and frontbackref
        triVars.zBackRef = -1; /// calculate a new z back
        triVars.zFrontRef = -1; /// calculate a new z ftont

        // try to add the triangle, and use the reference z height
        triVars.recursiveAttempt = false;
        bool wasAdded = attemptToAddTri(newTri, tokenHash, triVars, geomSpec);

        if (wasAdded) {
            // run again
            if (
                randN(
                    tokenHash,
                    string(
                        abi.encodePacked("addAdjRecursive", attemptNum, depth)
                    ),
                    0,
                    100
                ) <= geomSpec.probAdjRec
            ) {
                triVars = makeAdjacentTriangles(
                    tokenHash,
                    attemptNum,
                    triVars.nextTriIdx - 1,
                    triVars,
                    geomSpec,
                    sideIdx,
                    666, /// always make the next one 2/3 scale
                    depth + 1
                );
            }
        }
        return triVars;
    }

    /** @dev make triangles vertically opposite a reference triangle */
    function makeVerticallyOppositeTriangles(
        bytes32 tokenHash,
        uint256 attemptNum,
        uint256 refIdx,
        TriVars memory triVars,
        GeomSpec memory geomSpec,
        int256 overrideSideIdx,
        int256 overrideScale,
        int256 depth
    ) public view returns (TriVars memory) {
        /// get the side index (0, 1 or 2)
        int256 sideIdx;
        if (overrideSideIdx == -1) {
            sideIdx = randN(
                tokenHash,
                string(abi.encodePacked("vertOppSideIdx", attemptNum, depth)),
                0,
                2
            );
        } else {
            sideIdx = overrideSideIdx;
        }

        /// get the scale
        /// this value is scaled up by 1e3
        /// use attemptNum in seedModifier to ensure unique values each attempt
        int256 scale;
        if (geomSpec.varySize) {
            if (overrideScale == -1) {
                if (
                    // prettier-ignore
                    randN(
                        tokenHash,
                        string(abi.encodePacked("vertOppScale1", attemptNum, depth)),
                        0,
                        100
                    ) > 33
                ) {
                    // prettier-ignore
                    if (
                        randN(
                            tokenHash,
                            string(abi.encodePacked("vertOppScale2", attemptNum, depth)  ),
                            0,
                            100
                        ) > 50
                    ) {
                        scale = 1000; /// desired = 1 (same scale)
                    } else {
                        scale = 500; /// desired = 0.5 (half scale)
                    }
                } else {
                    scale = 2000; /// desired = 2 (double scale)
                }
            } else {
                scale = overrideScale;
            }
        } else {
            scale = 1e3;
        }

        /// make a new triangle
        int256[3][3] memory newTri = makeTriVertOpp(
            triVars.tris[refIdx],
            geomSpec,
            sideIdx,
            scale
        );

        /// set the zbackref and frontbackref
        triVars.zBackRef = -1; /// calculate a new z back
        triVars.zFrontRef = triVars.zFronts[refIdx];

        // try to add the triangle, and use the reference z height
        triVars.recursiveAttempt = false;
        bool wasAdded = attemptToAddTri(newTri, tokenHash, triVars, geomSpec);

        if (wasAdded) {
            /// run again
            if (
                randN(
                    tokenHash,
                    string(
                        abi.encodePacked("recursiveVertOpp", attemptNum, depth)
                    ),
                    0,
                    100
                ) <= geomSpec.probVertOppRec
            ) {
                triVars = makeVerticallyOppositeTriangles(
                    tokenHash,
                    attemptNum,
                    refIdx,
                    triVars,
                    geomSpec,
                    sideIdx,
                    666, /// always make the next one 2/3 scale
                    depth + 1
                );
            }
        }

        return triVars;
    }

    /** @dev place a triangle vertically opposite over the given point 
    @param refTri the reference triangle to base the new triangle on
    */
    function makeTriVertOpp(
        int256[3][3] memory refTri,
        GeomSpec memory geomSpec,
        int256 sideIdx,
        int256 scale
    ) internal view returns (int256[3][3] memory) {
        /// calculate the center of the reference triangle
        /// add and then divide by 1e3 (the factor by which scale is scaled up)
        int256 centerDist = (getRadiusLen(refTri) * (1e3 + scale)) / 1e3;

        /// get the new triangle's direction
        int256 newAngle = sideIdx *
            120 +
            60 +
            (isTriPointingUp(refTri) ? int256(60) : int256(0));

        int256 spacing = 64;

        /// calculate the true offset
        int256[3] memory offset = vector3RotateZ(
            [int256(0), centerDist + spacing, 0],
            newAngle
        );

        int256[3] memory centerVec = getCenterVec(refTri);
        int256[3] memory newCentre = ShackledMath.vector3Add(centerVec, offset);
        /// return the new triangle (div by 1e3 to account for scale)
        int256 newRadius = (scale * getRadiusLen(refTri)) / 1e3;
        newRadius = ShackledMath.min(geomSpec.maxTriRad, newRadius);
        newAngle -= 210;
        return makeTri(newCentre, newRadius, newAngle);
    }

    /** @dev make a new adjacent triangle
     */
    function makeTriAdjacent(
        bytes32 tokenHash,
        GeomSpec memory geomSpec,
        uint256 attemptNum,
        int256[3][3] memory refTri,
        int256 sideIdx,
        int256 scale,
        int256 depth
    ) internal view returns (int256[3][3] memory) {
        /// calculate the center of the new triangle
        /// add and then divide by 1e3 (the factor by which scale is scaled up)

        int256 centerDist = (getPerpLen(refTri) * (1e3 + scale)) / 1e3;

        /// get the new triangle's direction
        int256 newAngle = sideIdx *
            120 +
            (isTriPointingUp(refTri) ? int256(60) : int256(0));

        /// determine the direction of the offset offset
        /// get a unique random seed each attempt to ensure variation

        // prettier-ignore
        int256 offsetDirection = randN(
            tokenHash,
            string(abi.encodePacked("lateralOffset", attemptNum, depth)),
            0, 
            1
        ) 
        * 2 - 1;

        /// put if off to one side of the triangle if it's smaller
        /// scale is on order of 1e3
        int256 lateralOffset = (offsetDirection *
            (1e3 - scale) *
            getSideLen(refTri)) / 1e3;

        /// make a gap between the triangles
        int256 spacing = 6000;

        /// calculate the true offset
        int256[3] memory offset = vector3RotateZ(
            [lateralOffset, centerDist + spacing, 0],
            newAngle
        );

        int256[3] memory newCentre = ShackledMath.vector3Add(
            getCenterVec(refTri),
            offset
        );

        /// return the new triangle (div by 1e3 to account for scale)
        int256 newRadius = (scale * getRadiusLen(refTri)) / 1e3;
        newRadius = ShackledMath.min(geomSpec.maxTriRad, newRadius);
        newAngle -= 30;
        return makeTri(newCentre, newRadius, newAngle);
    }

    /** @dev  
    create a triangle centered at centre, 
    with length from centre to point of radius
    */
    function makeTri(
        int256[3] memory centre,
        int256 radius,
        int256 angle
    ) internal view returns (int256[3][3] memory tri) {
        /// create a vector to rotate around 3 times
        int256[3] memory offset = [radius, 0, 0];

        /// make 3 points of the tri
        for (uint256 i = 0; i < 3; i++) {
            int256 armAngle = 120 * int256(i);
            int256[3] memory offsetVec = vector3RotateZ(
                offset,
                armAngle + angle
            );

            tri[i] = ShackledMath.vector3Add(centre, offsetVec);
        }
    }

    /** @dev rotate a vector around x */
    function vector3RotateX(int256[3] memory v, int256 deg)
        internal
        view
        returns (int256[3] memory)
    {
        /// get the cos and sin of the angle
        (int256 cos, int256 sin) = trigHelper(deg);

        /// calculate new y and z (scaling down to account for trig scaling)
        int256 y = ((v[1] * cos) - (v[2] * sin)) / 1e18;
        int256 z = ((v[1] * sin) + (v[2] * cos)) / 1e18;
        return [v[0], y, z];
    }

    /** @dev rotate a vector around y */
    function vector3RotateY(int256[3] memory v, int256 deg)
        internal
        view
        returns (int256[3] memory)
    {
        /// get the cos and sin of the angle
        (int256 cos, int256 sin) = trigHelper(deg);

        /// calculate new x and z (scaling down to account for trig scaling)
        int256 x = ((v[0] * cos) - (v[2] * sin)) / 1e18;
        int256 z = ((v[0] * sin) + (v[2] * cos)) / 1e18;
        return [x, v[1], z];
    }

    /** @dev rotate a vector around z */
    function vector3RotateZ(int256[3] memory v, int256 deg)
        internal
        view
        returns (int256[3] memory)
    {
        /// get the cos and sin of the angle
        (int256 cos, int256 sin) = trigHelper(deg);

        /// calculate new x and y (scaling down to account for trig scaling)
        int256 x = ((v[0] * cos) - (v[1] * sin)) / 1e18;
        int256 y = ((v[0] * sin) + (v[1] * cos)) / 1e18;
        return [x, y, v[2]];
    }

    /** @dev calculate sin and cos of an angle */
    function trigHelper(int256 deg)
        internal
        view
        returns (int256 cos, int256 sin)
    {
        /// deal with negative degrees here, since Trigonometry.sol can't
        int256 n360 = (ShackledMath.abs(deg) / 360) + 1;
        deg = (deg + (360 * n360)) % 360;
        uint256 rads = uint256((deg * PI) / 180);
        /// calculate radians (in 1e18 space)
        cos = Trigonometry.cos(rads);
        sin = Trigonometry.sin(rads);
    }

    /** @dev Get the 3d vector at the center of a triangle */
    function getCenterVec(int256[3][3] memory tri)
        internal
        view
        returns (int256[3] memory)
    {
        return
            ShackledMath.vector3DivScalar(
                ShackledMath.vector3Add(
                    ShackledMath.vector3Add(tri[0], tri[1]),
                    tri[2]
                ),
                3
            );
    }

    /** @dev Get the length from the center of a triangle to point*/
    function getRadiusLen(int256[3][3] memory tri)
        internal
        view
        returns (int256)
    {
        return
            ShackledMath.vector3Len(
                ShackledMath.vector3Sub(getCenterVec(tri), tri[0])
            );
    }

    /** @dev Get the length from any point on triangle to other point (equilateral)*/
    function getSideLen(int256[3][3] memory tri)
        internal
        view
        returns (int256)
    {
        // len * 0.886
        return (getRadiusLen(tri) * 8660) / 10000;
    }

    /** @dev Get the shortes length from center of triangle to side */
    function getPerpLen(int256[3][3] memory tri)
        internal
        view
        returns (int256)
    {
        return getRadiusLen(tri) / 2;
    }

    /** @dev Determine if a triangle is pointing up*/
    function isTriPointingUp(int256[3][3] memory tri)
        internal
        view
        returns (bool)
    {
        int256 centerY = getCenterVec(tri)[1];
        /// count how many verts are above this y value
        int256 nAbove = 0;
        for (uint256 i = 0; i < 3; i++) {
            if (tri[i][1] > centerY) {
                nAbove++;
            }
        }
        return nAbove == 1;
    }

    /** @dev check if two triangles are close */
    function areTrisClose(int256[3][3] memory tri1, int256[3][3] memory tri2)
        internal
        view
        returns (bool)
    {
        int256 lenBetweenCenters = ShackledMath.vector3Len(
            ShackledMath.vector3Sub(getCenterVec(tri1), getCenterVec(tri2))
        );
        return lenBetweenCenters < (getPerpLen(tri1) + getPerpLen(tri2));
    }

    /** @dev check if two triangles have overlapping points*/
    function areTrisPointsOverlapping(
        int256[3][3] memory tri1,
        int256[3][3] memory tri2
    ) internal view returns (bool) {
        /// check triangle a against b
        if (
            isPointInTri(tri1, tri2[0]) ||
            isPointInTri(tri1, tri2[1]) ||
            isPointInTri(tri1, tri2[2])
        ) {
            return true;
        }

        /// check triangle b against a
        if (
            isPointInTri(tri2, tri1[0]) ||
            isPointInTri(tri2, tri1[1]) ||
            isPointInTri(tri2, tri1[2])
        ) {
            return true;
        }

        /// otherwise they mustn't be overlapping
        return false;
    }

    /** @dev calculate if a point is in a tri*/
    function isPointInTri(int256[3][3] memory tri, int256[3] memory p)
        internal
        view
        returns (bool)
    {
        int256[3] memory p1 = tri[0];
        int256[3] memory p2 = tri[1];
        int256[3] memory p3 = tri[2];
        int256 alphaNum = (p2[1] - p3[1]) *
            (p[0] - p3[0]) +
            (p3[0] - p2[0]) *
            (p[1] - p3[1]);

        int256 alphaDenom = (p2[1] - p3[1]) *
            (p1[0] - p3[0]) +
            (p3[0] - p2[0]) *
            (p1[1] - p3[1]);

        int256 betaNum = (p3[1] - p1[1]) *
            (p[0] - p3[0]) +
            (p1[0] - p3[0]) *
            (p[1] - p3[1]);

        int256 betaDenom = (p2[1] - p3[1]) *
            (p1[0] - p3[0]) +
            (p3[0] - p2[0]) *
            (p1[1] - p3[1]);

        if (alphaDenom == 0 || betaDenom == 0) {
            return false;
        } else {
            int256 alpha = (alphaNum * 1e6) / alphaDenom;
            int256 beta = (betaNum * 1e6) / betaDenom;

            int256 gamma = 1e6 - alpha - beta;
            return alpha > 0 && beta > 0 && gamma > 0;
        }
    }

    /** @dev check all points of the tri to see if it overlaps with any other tris
     */
    function isTriOverlappingWithTris(
        int256[3][3] memory tri,
        int256[3][3][] memory tris,
        uint256 nextTriIdx
    ) internal view returns (bool) {
        /// check against all other tris added thus fat
        for (uint256 i = 0; i < nextTriIdx; i++) {
            if (
                areTrisClose(tri, tris[i]) ||
                areTrisPointsOverlapping(tri, tris[i])
            ) {
                return true;
            }
        }
        return false;
    }

    function isPointCloseToLine(
        int256[3] memory p,
        int256[3] memory l1,
        int256[3] memory l2
    ) internal view returns (bool) {
        int256 x0 = p[0];
        int256 y0 = p[1];
        int256 x1 = l1[0];
        int256 y1 = l1[1];
        int256 x2 = l2[0];
        int256 y2 = l2[1];
        int256 distanceNum = ShackledMath.abs(
            (x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1)
        );
        int256 distanceDenom = ShackledMath.hypot((x2 - x1), (y2 - y1));
        int256 distance = distanceNum / distanceDenom;
        if (distance < 8) {
            return true;
        }
    }

    /** compare a triangles points against the lines of other tris */
    function isTrisPointsCloseToLines(
        int256[3][3] memory tri,
        int256[3][3][] memory tris,
        uint256 nextTriIdx
    ) internal view returns (bool) {
        for (uint256 i = 0; i < nextTriIdx; i++) {
            for (uint256 p = 0; p < 3; p++) {
                if (isPointCloseToLine(tri[p], tris[i][0], tris[i][1])) {
                    return true;
                }
                if (isPointCloseToLine(tri[p], tris[i][1], tris[i][2])) {
                    return true;
                }
                if (isPointCloseToLine(tri[p], tris[i][2], tris[i][0])) {
                    return true;
                }
            }
        }
    }

    /** @dev check if tri to add meets certain criteria */
    function isTriLegal(
        int256[3][3] memory tri,
        int256[3][3][] memory tris,
        uint256 nextTriIdx,
        int256 minTriRad
    ) internal view returns (bool) {
        // check radius first as point checks will fail
        // if the radius is too small
        if (getRadiusLen(tri) < minTriRad) {
            return false;
        }
        return (!isTriOverlappingWithTris(tri, tris, nextTriIdx) &&
            !isTrisPointsCloseToLines(tri, tris, nextTriIdx));
    }

    /** @dev helper function to add triangles */
    function attemptToAddTri(
        int256[3][3] memory tri,
        bytes32 tokenHash,
        TriVars memory triVars,
        GeomSpec memory geomSpec
    ) internal view returns (bool added) {
        bool isLegal = isTriLegal(
            tri,
            triVars.tris,
            triVars.nextTriIdx,
            geomSpec.minTriRad
        );
        if (isLegal && triVars.nextTriIdx < geomSpec.maxPrisms) {
            // add the triangle
            triVars.tris[triVars.nextTriIdx] = tri;
            added = true;

            // add the new zs
            if (triVars.zBackRef == -1) {
                /// z back ref is not provided, calculate it
                triVars.zBacks[triVars.nextTriIdx] = calculateZ(
                    tri,
                    tokenHash,
                    triVars.nextTriIdx,
                    geomSpec,
                    false
                );
            } else {
                /// use the provided z back (from the ref)
                triVars.zBacks[triVars.nextTriIdx] = triVars.zBackRef;
            }
            if (triVars.zFrontRef == -1) {
                /// z front ref is not provided, calculate it
                triVars.zFronts[triVars.nextTriIdx] = calculateZ(
                    tri,
                    tokenHash,
                    triVars.nextTriIdx,
                    geomSpec,
                    true
                );
            } else {
                /// use the provided z front (from the ref)
                triVars.zFronts[triVars.nextTriIdx] = triVars.zFrontRef;
            }

            // increment the tris counter
            triVars.nextTriIdx += 1;

            // if we're using any type of symmetry then attempt to add a symmetric triangle
            // only do this recursively once
            if (
                (geomSpec.isSymmetricX || geomSpec.isSymmetricY) &&
                (!triVars.recursiveAttempt)
            ) {
                int256[3][3] memory symTri = copyTri(tri);

                if (geomSpec.isSymmetricX) {
                    symTri[0][0] = -symTri[0][0];
                    symTri[1][0] = -symTri[1][0];
                    symTri[2][0] = -symTri[2][0];
                    // symCenter[0] = -symCenter[0];
                }

                if (geomSpec.isSymmetricY) {
                    symTri[0][1] = -symTri[0][1];
                    symTri[1][1] = -symTri[1][1];
                    symTri[2][1] = -symTri[2][1];
                    // symCenter[1] = -symCenter[1];
                }

                if (
                    (geomSpec.isSymmetricX || geomSpec.isSymmetricY) &&
                    !(geomSpec.isSymmetricX && geomSpec.isSymmetricY)
                ) {
                    symTri = [symTri[2], symTri[1], symTri[0]];
                }

                triVars.recursiveAttempt = true;
                triVars.zBackRef = triVars.zBacks[triVars.nextTriIdx - 1];
                triVars.zFrontRef = triVars.zFronts[triVars.nextTriIdx - 1];
                attemptToAddTri(symTri, tokenHash, triVars, geomSpec);
            }
        }
    }

    /** @dev rotate a triangle by x, y, or z 
    @param axis 0 = x, 1 = y, 2 = z
    */
    function triRotHelp(
        int256 axis,
        int256[3][3] memory tri,
        int256 rot
    ) internal view returns (int256[3][3] memory) {
        if (axis == 0) {
            return [
                vector3RotateX(tri[0], rot),
                vector3RotateX(tri[1], rot),
                vector3RotateX(tri[2], rot)
            ];
        } else if (axis == 1) {
            return [
                vector3RotateY(tri[0], rot),
                vector3RotateY(tri[1], rot),
                vector3RotateY(tri[2], rot)
            ];
        } else if (axis == 2) {
            return [
                vector3RotateZ(tri[0], rot),
                vector3RotateZ(tri[1], rot),
                vector3RotateZ(tri[2], rot)
            ];
        }
    }

    /** @dev a helper to run rotation functions on back/front triangles */
    function triBfHelp(
        int256 axis,
        int256[3][3][] memory trisBack,
        int256[3][3][] memory trisFront,
        int256 rot
    ) internal view returns (int256[3][3][] memory, int256[3][3][] memory) {
        int256[3][3][] memory trisBackNew = new int256[3][3][](trisBack.length);
        int256[3][3][] memory trisFrontNew = new int256[3][3][](
            trisFront.length
        );

        for (uint256 i = 0; i < trisBack.length; i++) {
            trisBackNew[i] = triRotHelp(axis, trisBack[i], rot);
            trisFrontNew[i] = triRotHelp(axis, trisFront[i], rot);
        }

        return (trisBackNew, trisFrontNew);
    }

    /** @dev get the maximum extent of the geometry (vertical or horizontal) */
    function getExtents(int256[3][3][] memory tris)
        internal
        view
        returns (int256[3][2] memory)
    {
        int256 minX = MAX_INT;
        int256 maxX = MIN_INT;
        int256 minY = MAX_INT;
        int256 maxY = MIN_INT;
        int256 minZ = MAX_INT;
        int256 maxZ = MIN_INT;

        for (uint256 i = 0; i < tris.length; i++) {
            for (uint256 j = 0; j < tris[i].length; j++) {
                minX = ShackledMath.min(minX, tris[i][j][0]);
                maxX = ShackledMath.max(maxX, tris[i][j][0]);
                minY = ShackledMath.min(minY, tris[i][j][1]);
                maxY = ShackledMath.max(maxY, tris[i][j][1]);
                minZ = ShackledMath.min(minZ, tris[i][j][2]);
                maxZ = ShackledMath.max(maxZ, tris[i][j][2]);
            }
        }
        return [[minX, minY, minZ], [maxX, maxY, maxZ]];
    }

    /** @dev go through each triangle and apply a 'height' */
    function calculateZ(
        int256[3][3] memory tri,
        bytes32 tokenHash,
        uint256 nextTriIdx,
        GeomSpec memory geomSpec,
        bool front
    ) internal view returns (int256) {
        int256 h;
        string memory seedMod = string(abi.encodePacked("calcZ", nextTriIdx));
        if (front) {
            if (geomSpec.id == 6) {
                h = 1;
            } else {
                if (randN(tokenHash, seedMod, 0, 10) > 9) {
                    if (randN(tokenHash, seedMod, 0, 10) > 3) {
                        h = 10;
                    } else {
                        h = 22;
                    }
                } else {
                    if (randN(tokenHash, seedMod, 0, 10) > 5) {
                        h = 8;
                    } else {
                        h = 1;
                    }
                }
            }
        } else {
            if (geomSpec.id == 6) {
                h = -1;
            } else {
                if (geomSpec.id == 5) {
                    h = -randN(tokenHash, seedMod, 2, 20);
                } else {
                    h = -2;
                }
            }
        }
        if (geomSpec.id == 5) {
            h += 10;
        }
        return h * geomSpec.depthMultiplier;
    }

    /** @dev roll a specId given a list of weightings */
    function getSpecId(bytes32 tokenHash, int256[2][7] memory weightings)
        internal
        view
        returns (uint256)
    {
        int256 n = GeomUtils.randN(
            tokenHash,
            "specId",
            weightings[0][0],
            weightings[weightings.length - 1][1]
        );
        for (uint256 i = 0; i < weightings.length; i++) {
            if (weightings[i][0] <= n && n <= weightings[i][1]) {
                return i;
            }
        }
    }

    /** @dev get a random number between two numbers
    with a uniform probability distribution
    @param randomSeed a hash that we can use to 'randomly' get a number 
    @param seedModifier some string to make the result unique for this tokenHash
    @param min the minimum number (inclusive)
    @param max the maximum number (inclusive)

    examples:
        to get binary output (0 or 1), set min as 0 and max as 1
        
     */
    function randN(
        bytes32 randomSeed,
        string memory seedModifier,
        int256 min,
        int256 max
    ) internal view returns (int256) {
        /// use max() to ensure modulo != 0
        return
            int256(
                uint256(keccak256(abi.encodePacked(randomSeed, seedModifier))) %
                    uint256(ShackledMath.max(1, (max + 1 - min)))
            ) + min;
    }

    /** @dev clip an array of tris to a certain length (to trim empty tail slots) */
    function clipTrisToLength(int256[3][3][] memory arr, uint256 desiredLen)
        internal
        view
        returns (int256[3][3][] memory)
    {
        uint256 n = arr.length - desiredLen;
        assembly {
            mstore(arr, sub(mload(arr), n))
        }
        return arr;
    }

    /** @dev clip an array of Z values to a certain length (to trim empty tail slots) */
    function clipZsToLength(int256[] memory arr, uint256 desiredLen)
        internal
        view
        returns (int256[] memory)
    {
        uint256 n = arr.length - desiredLen;
        assembly {
            mstore(arr, sub(mload(arr), n))
        }
        return arr;
    }

    /** @dev make a copy of a triangle */
    function copyTri(int256[3][3] memory tri)
        internal
        view
        returns (int256[3][3] memory)
    {
        return [
            [tri[0][0], tri[0][1], tri[0][2]],
            [tri[1][0], tri[1][1], tri[1][2]],
            [tri[2][0], tri[2][1], tri[2][2]]
        ];
    }

    /** @dev make a copy of an array of triangles */
    function copyTris(int256[3][3][] memory tris)
        internal
        view
        returns (int256[3][3][] memory)
    {
        int256[3][3][] memory newTris = new int256[3][3][](tris.length);
        for (uint256 i = 0; i < tris.length; i++) {
            newTris[i] = copyTri(tris[i]);
        }
        return newTris;
    }
}
/**
 Hold some functions externally to reduce contract size for mainnet deployment
 */
function areTrisClose(int256[3][3] memory tri1, int256[3][3] memory tri2)
    internal
    view
    returns (bool)
{
    int256 lenBetweenCenters = ShackledMath.vector3Len(
        ShackledMath.vector3Sub(getCenterVec(tri1), getCenterVec(tri2))
    );
    return lenBetweenCenters < (getPerpLen(tri1) + getPerpLen(tri2));
}

/** @dev check if two triangles are close */