private static SNode loadNode(Elem root) throws Exception {
    u.p("loading SVG element: " + root.name());
    if ("g".equals(root.name())) {
      SGroup g = new SGroup();
      for (Elem n : root.xpath("./*")) {
        SNode nn = loadNode(n);
        if (nn != null) g.addAll(false, nn);
      }
      g.normalize();
      return g;
    }
    if ("rect".equals(root.name())) {
      SRect rect = new SRect();
      rect.setX(Double.parseDouble(root.attr("x")));
      rect.setY(Double.parseDouble(root.attr("y")));
      parseFill(rect, root);
      parseStroke(rect, root);
      rect.setWidth(Double.parseDouble(root.attr("width")));
      rect.setHeight(Double.parseDouble(root.attr("height")));
      return rect;
    }
    if ("line".equals(root.name())) {
      SPoly poly = new SPoly();
      double x1 = Double.parseDouble(root.attr("x1"));
      double y1 = Double.parseDouble(root.attr("y1"));
      double x2 = Double.parseDouble(root.attr("x2"));
      double y2 = Double.parseDouble(root.attr("y2"));
      poly.addPoint(new Point2D.Double(x1, y1));
      poly.addPoint(new Point2D.Double(x2, y2));
      poly.setClosed(false);
      parseFill(poly, root);
      parseStroke(poly, root);
      return poly;
    }
    if ("polygon".equals(root.name()) || "polyline".equals(root.name())) {
      String pointsString = root.attr("points");
      String[] points = pointsString.split("\\s");
      SPoly poly = new SPoly();
      for (String pt : points) {
        if (pt != null && pt.trim().equals("")) continue;
        String[] xy = pt.split(",");
        poly.addPoint(new Point2D.Double(Double.parseDouble(xy[0]), Double.parseDouble(xy[1])));
      }
      // the polygon is closed, poly lines aren't
      poly.setClosed("polygon".equals(root.name()));
      parseFill(poly, root);
      parseStroke(poly, root);
      return poly;
    }
    if ("path".equals(root.name())) {
      return parsePathNode(root);
    }

    Exception ex = new Exception("unrecognized SVG element: " + root.name());
    u.p(ex);
    return null;
  }
    @Override
    public void execute() {
      if (context.getSelection().size() < 2) return;
      SketchDocument doc = context.getDocument();

      final List<SNode> model = doc.getCurrentPage().getModel();

      final List<SNode> nodes = new ArrayList<SNode>();
      for (SNode node : model) {
        if (context.getSelection().contains(node)) {
          nodes.add(node);
        }
      }

      model.removeAll(nodes);
      final SGroup group = new SGroup();
      group.addAll(nodes);
      model.add(group);
      context.getSelection().clear();
      context.getSelection().setSelectedNode(group);
      context.redraw();

      UndoManager.UndoableAction action =
          new UndoManager.UndoableAction() {
            public void executeUndo() {
              model.remove(group);
              for (SNode node : nodes) {
                model.add(node);
                node.setTranslateX(node.getTranslateX() + group.getTranslateX());
                node.setTranslateY(node.getTranslateY() + group.getTranslateY());
              }
              context.getSelection().setSelectedNodes(nodes);
              context.redraw();
            }

            public void executeRedo() {
              model.removeAll(nodes);
              group.addAll(nodes);
              model.add(group);
              context.getSelection().setSelectedNode(group);
              context.redraw();
            }

            public CharSequence getName() {
              return "group shapes";
            }
          };
      context.getUndoManager().pushAction(action);
    }
    @Override
    public void execute() {
      if (context.getSelection().size() != 1) return;
      SNode n = context.getSelection().items().iterator().next();
      if (!(n instanceof SGroup)) return;
      final SGroup group = (SGroup) n;

      SketchDocument doc = context.getDocument();
      final List<SNode> model = doc.getCurrentPage().getModel();
      model.remove(group);
      model.addAll(group.getNodes());
      context.getSelection().clear();
      for (SNode node : group.getNodes()) {
        node.setTranslateX(node.getTranslateX() + group.getTranslateX());
        node.setTranslateY(node.getTranslateY() + group.getTranslateY());
        context.getSelection().addSelectedNode(node);
      }
      context.redraw();
      UndoManager.UndoableAction action =
          new UndoManager.UndoableAction() {
            public void executeUndo() {
              model.removeAll(group.getNodes());
              for (SNode node : group.getNodes()) {
                node.setTranslateX(node.getTranslateX() - group.getTranslateX());
                node.setTranslateY(node.getTranslateY() - group.getTranslateY());
              }
              model.add(group);
              context.getSelection().setSelectedNode(group);
              context.redraw();
            }

            public void executeRedo() {
              model.remove(group);
              for (SNode node : group.getNodes()) {
                model.add(node);
                node.setTranslateX(node.getTranslateX() + group.getTranslateX());
                node.setTranslateY(node.getTranslateY() + group.getTranslateY());
              }
              context.getSelection().setSelectedNodes(group.getNodes());
              context.redraw();
            }

            public CharSequence getName() {
              return "ungroup shapes";
            }
          };
      context.getUndoManager().pushAction(action);
    }