createShapes static method

List<Shape> createShapes(
  1. ShapePath shapePath
)

Implementation

static List<Shape> createShapes(ShapePath shapePath) {
  // Param shapePath: a shapepath as returned by the parse function of this class
  // Returns Shape object

  const bigNumber = 99999999999999.0;

  final intersectionLocationType = {
    "ORIGIN": 0,
    "DESTINATION": 1,
    "BETWEEN": 2,
    "LEFT": 3,
    "RIGHT": 4,
    "BEHIND": 5,
    "BEYOND": 6
  };

  Map<String, dynamic> classifyResult = {
    "loc": intersectionLocationType["ORIGIN"],
    "t": 0
  };

  classifyPoint(p, edgeStart, edgeEnd) {
    final ax = edgeEnd.x - edgeStart.x;
    final ay = edgeEnd.y - edgeStart.y;
    final bx = p.x - edgeStart.x;
    final by = p.y - edgeStart.y;
    final sa = ax * by - bx * ay;

    if ((p.x == edgeStart.x) && (p.y == edgeStart.y)) {
      classifyResult["loc"] = intersectionLocationType["ORIGIN"];
      classifyResult["t"] = 0;
      return;
    }

    if ((p.x == edgeEnd.x) && (p.y == edgeEnd.y)) {
      classifyResult["loc"] = intersectionLocationType["DESTINATION"];
      classifyResult["t"] = 1;
      return;
    }

    if (sa < -MathUtils.epsilon) {
      classifyResult["loc"] = intersectionLocationType["LEFT"];
      return;
    }

    if (sa > MathUtils.epsilon) {
      classifyResult["loc"] = intersectionLocationType["RIGHT"];
      return;
    }

    if (((ax * bx) < 0) || ((ay * by) < 0)) {
      classifyResult["loc"] = intersectionLocationType["BEHIND"];
      return;
    }

    if ((math.sqrt(ax * ax + ay * ay)) < (math.sqrt(bx * bx + by * by))) {
      classifyResult["loc"] = intersectionLocationType["BEYOND"];
      return;
    }

    dynamic t;

    if (ax != 0) {
      t = bx / ax;
    }
    else {
      t = by / ay;
    }

    classifyResult["loc"] = intersectionLocationType["BETWEEN"];
    classifyResult["t"] = t;
  }

  findEdgeIntersection(a0, a1, b0, b1) {
    final x1 = a0.x;
    final x2 = a1.x;
    final x3 = b0.x;
    final x4 = b1.x;
    final y1 = a0.y;
    final y2 = a1.y;
    final y3 = b0.y;
    final y4 = b1.y;
    final nom1 = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
    final nom2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
    final denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
    final t1 = nom1 / denom;
    final t2 = nom2 / denom;

    if (((denom == 0) && (nom1 != 0)) ||
        (t1 <= 0) ||
        (t1 >= 1) ||
        (t2 < 0) ||
        (t2 > 1)) {
      //1. lines are parallel or edges don't intersect

      return null;
    } else if ((nom1 == 0) && (denom == 0)) {
      //2. lines are colinear

      //check if endpoints of edge2 (b0-b1) lies on edge1 (a0-a1)
      for (int i = 0; i < 2; i++) {
        classifyPoint(i == 0 ? b0 : b1, a0, a1);
        //find position of this endpoints relatively to edge1
        if (classifyResult["loc"] == intersectionLocationType["ORIGIN"]) {
          final point = (i == 0 ? b0 : b1);
          return {"x": point.x, "y": point.y, "t": classifyResult["t"]};
        } else if (classifyResult["loc"] ==
            intersectionLocationType["BETWEEN"]) {
          final x = num.parse((x1 + classifyResult["t"]! * (x2 - x1))
              .toStringAsPrecision(10));
          final y = num.parse((y1 + classifyResult["t"]! * (y2 - y1))
              .toStringAsPrecision(10));
          return {"x": x, "y": y, "t": classifyResult["t"]};
        }
      }

      return null;
    }
    else {
      //3. edges intersect

      for (int i = 0; i < 2; i++) {
        classifyPoint(i == 0 ? b0 : b1, a0, a1);

        if (classifyResult["loc"] == intersectionLocationType["ORIGIN"]) {
          final point = (i == 0 ? b0 : b1);
          return {"x": point.x, "y": point.y, "t": classifyResult["t"]};
        }
      }

      final x = num.parse((x1 + t1 * (x2 - x1)).toStringAsPrecision(10));
      final y = num.parse((y1 + t1 * (y2 - y1)).toStringAsPrecision(10));
      return {"x": x, "y": y, "t": t1};
    }
  }

  getIntersections(path1, path2) {
    final intersectionsRaw = [];
    final intersections = [];

    for (int index = 1; index < path1.length; index++) {
      final path1EdgeStart = path1[index - 1];
      final path1EdgeEnd = path1[index];

      for (int index2 = 1; index2 < path2.length; index2++) {
        final path2EdgeStart = path2[index2 - 1];
        final path2EdgeEnd = path2[index2];

        final intersection = findEdgeIntersection(
            path1EdgeStart, path1EdgeEnd, path2EdgeStart, path2EdgeEnd);

        if (intersection != null &&
            intersectionsRaw.indexWhere((i) =>
                    i["t"] <= intersection["t"] + MathUtils.epsilon &&
                    i["t"] >= intersection["t"] - MathUtils.epsilon) <
                0) {
          intersectionsRaw.add(intersection);
          intersections.add(Vector3(intersection["x"], intersection["y"]));
        }
      }
    }

    return intersections;
  }

  getScanlineIntersections(scanline, boundingBox, paths) {
    final center = Vector3();
    boundingBox.getCenter(center);

    final allIntersections = [];

    paths.forEach((path) {
      // check if the center of the bounding box is in the bounding box of the paths.
      // this is a pruning method to limit the search of intersections in paths that can't envelop of the current path.
      // if a path envelops another path. The center of that oter path, has to be inside the bounding box of the enveloping path.
      if (path["boundingBox"].containsPoint(center)) {
        final intersections = getIntersections(scanline, path["points"]);

        for (final p in intersections) {
          allIntersections.add({
            "identifier": path["identifier"],
            "isCW": path["isCW"],
            "point": p
          });
        }
      }
    });

    allIntersections.sort((i1, i2) {
      return i1["point"].x >= i2["point"].x ? 1 : -1;
    });

    return allIntersections;
  }

  isHoleTo(simplePath, allPaths, scanlineMinX, scanlineMaxX, _fillRule) {
    if (_fillRule == null || _fillRule == '') {
      _fillRule = 'nonzero';
    }

    final centerBoundingBox = Vector3();
    simplePath["boundingBox"].getCenter(centerBoundingBox);

    final scanline = [
      Vector3(scanlineMinX, centerBoundingBox.y),
      Vector3(scanlineMaxX, centerBoundingBox.y)
    ];

    final scanlineIntersections = getScanlineIntersections(
        scanline, simplePath["boundingBox"], allPaths);

    scanlineIntersections.sort((i1, i2) {
      return i1["point"].x >= i2["point"].x ? 1 : -1;
    });

    final baseIntersections = [];
    final otherIntersections = [];

    for (final i in scanlineIntersections) {
      if (i["identifier"] == simplePath["identifier"]) {
        baseIntersections.add(i);
      } else {
        otherIntersections.add(i);
      }
    }

    final firstXOfPath = baseIntersections[0]["point"].x;

    // build up the path hierarchy
    final stack = [];
    int i = 0;

    while (i < otherIntersections.length &&
        otherIntersections[i]["point"].x < firstXOfPath) {
      if (stack.isNotEmpty &&
          stack[stack.length - 1] == otherIntersections[i]["identifier"]) {
        stack.removeLast();
      } else {
        stack.add(otherIntersections[i]["identifier"]);
      }

      i++;
    }

    stack.add(simplePath["identifier"]);

    if (_fillRule == 'evenodd') {
      final isHole = stack.length % 2 == 0 ? true : false;
      final isHoleFor = stack[stack.length - 2];

      return {
        "identifier": simplePath["identifier"],
        "isHole": isHole,
        "for": isHoleFor
      };
    }
    else if (_fillRule == 'nonzero') {
      // check if path is a hole by counting the amount of paths with alternating rotations it has to cross.
      bool isHole = true;
      late int isHoleFor;
      late bool lastCWValue;

      for (int i = 0; i < stack.length; i++) {
        final identifier = stack[i];
        if (isHole) {
          lastCWValue = allPaths[identifier]["isCW"];
          isHole = false;
          isHoleFor = identifier;
        }
        else if (lastCWValue != allPaths[identifier]["isCW"]) {
          lastCWValue = allPaths[identifier]["isCW"];
          isHole = true;
        }
      }

      return {
        "identifier": simplePath["identifier"],
        "isHole": isHole,
        "for": isHoleFor
      };
    } else {
      console.info('fill-rule: $_fillRule is currently not implemented.');
    }
  }

  // check for self intersecting paths
  // TODO

  // check intersecting paths
  // TODO

  // prepare paths for hole detection
  int identifier = 0;

  num scanlineMinX = bigNumber;
  num scanlineMaxX = -bigNumber;

  List simplePaths = shapePath.subPaths.map((p) {
    final points = p.getPoints();
    double maxY = -bigNumber;
    double minY = bigNumber;
    double maxX = -bigNumber;
    double minX = bigNumber;

    //points.forEach(p => p.y *= -1);

    for (int i = 0; i < points.length; i++) {
      final p = points[i]!;

      if (p.y > maxY) {
        maxY = p.y;
      }

      if (p.y < minY) {
        minY = p.y;
      }

      if (p.x > maxX) {
        maxX = p.x;
      }

      if (p.x < minX) {
        minX = p.x;
      }
    }

    //
    if (scanlineMaxX <= maxX) {
      scanlineMaxX = maxX + 1;
    }

    if (scanlineMinX >= minX) {
      scanlineMinX = minX - 1;
    }

    return {
      "curves": p.curves,
      "points": points,
      "isCW": ShapeUtils.isClockWise(points),
      "identifier": identifier++,
      "boundingBox": BoundingBox(Vector3(minX, minY,0), Vector3(maxX, maxY,0))
    };
  }).toList();

  simplePaths = simplePaths.where((sp) {
    return sp["points"].length > 1;
  }).toList();

  // check if path is solid or a hole
  final isAHole = simplePaths
      .map((p) => isHoleTo(p, simplePaths, scanlineMinX, scanlineMaxX,
          shapePath.userData!["style"]["fillRule"]))
      .toList();

  final List<Shape> shapesToReturn = [];
  for (final p in simplePaths) {
    final amIAHole = isAHole[p["identifier"]];

    if (amIAHole?["isHole"] == false) {
      final shape = Shape(null);
      shape.curves = p["curves"];
      final holes = isAHole
          .where((h) => h!["isHole"] && h["for"] == p["identifier"])
          .toList();
      for (final h in holes) {
        final hole = simplePaths[h!["identifier"]];
        final path = Path(null);
        path.curves = hole["curves"];
        shape.holes.add(path);
      }
      shapesToReturn.add(shape);
    }
  }

  return shapesToReturn;
}