def add_breadth_line_of_sibling_nodes(self, parent_id: tp.Tuple[str, int], start_coord: tp.Tuple[int, int], \
                                          end_coord: tp.Tuple[int, int], \
                                          node_specs: tp.List[tp.Optional[NodeSpec]] \
                                          ) -> tp.List[int]:

        num_specs = len(node_specs)
        parent_id = self._id_if_str(parent_id)
        if num_specs < 2:
            raise ValueError("node_specs must have at least 2 elements")
        if node_specs[0] is None or node_specs[-1] is None:
            raise ValueError(
                "The first and last item of node_specs must not be None")

        added_ids = []
        start_vec2 = angles.vec2(start_coord)
        end_vec2 = angles.vec2(end_coord)
        rel_vec2 = end_vec2 - start_vec2

        count = 0
        for spec in node_specs:
            if spec is not None:
                pos = start_vec2 + rel_vec2 * count / (num_specs - 1)
                new_id = self.add_node(spec.text, pos, spec.node_col,
                                       spec.multibox)
                if spec.link_draw == ArrowDraw.BACK_ARROW:
                    self.add_link(new_id, parent_id, spec.link_col,
                                  ArrowDraw.FWD_ARROW, None)
                elif spec.link_draw != ArrowDraw.NO_LINK:
                    self.add_link(parent_id, new_id, spec.link_col,
                                  spec.link_draw, spec.link_2_col)

                added_ids.append(new_id)
            count += 1

        return added_ids
    def _draw_arrowhead(self, surface, from_coord: tp.Tuple[int, int], to_coord: tp.Tuple[int, int], \
                        dry_run: bool=False, is_second_link: bool=False) -> bool:
        """
        Calculates where to draw the arrowhead based on where the link would be visible between
        the two nodes, i.e. from the two intersection points.

        Returns False if, during the above calculation, it discovers the nodes are overlapping.
        That means the link would be entirely obscured.
        """

        from_vec2 = angles.vec2(from_coord)
        to_vec2 = angles.vec2(to_coord)

        if not is_second_link:
            rel_vec_frac_from, intersection_from = \
                self._from_node.get_intersection_point_to_link(from_vec2, to_vec2)
            rel_vec_frac_to, intersection_to = \
                self._to_node.get_intersection_point_to_link(to_vec2, from_vec2)
        else:
            rel_vec_frac_from, intersection_from = \
                self._to_node.get_intersection_point_to_link(to_vec2, from_vec2)
            rel_vec_frac_to, intersection_to = \
                self._from_node.get_intersection_point_to_link(from_vec2, to_vec2)
        rel_vec_frac_to = 1 - rel_vec_frac_to

        if rel_vec_frac_to < rel_vec_frac_from:
            # The nodes have overlapped, the link would be completely obscured
            return False

        if dry_run or self._arrow_draw == ArrowDraw.NO_ARROW:
            return True

        rel_vec2 = intersection_to - intersection_from
        draw_arrow_at = intersection_from + (rel_vec2 * 2 / 3)

        left_endpoint, right_endpoint = self._get_arrow_endpoints(
            rel_vec2, draw_arrow_at)

        colour = self._second_colour if is_second_link \
            else self._colour

        pygame.draw.line(surface, colour, draw_arrow_at, left_endpoint,
                         self._width)
        pygame.draw.line(surface, colour, draw_arrow_at, right_endpoint,
                         self._width)

        if not is_second_link and self._arrow_draw == ArrowDraw.DOUBLE_ARROW:
            draw_arrow_at = intersection_from + (rel_vec2 * 1 / 3)

            left_endpoint, right_endpoint = self._get_arrow_endpoints(
                -rel_vec2, draw_arrow_at)

            pygame.draw.line(surface, colour, draw_arrow_at, left_endpoint,
                             self._width)
            pygame.draw.line(surface, colour, draw_arrow_at, right_endpoint,
                             self._width)

        return True
    def add_arc_of_sibling_nodes(self, parent_id: tp.Tuple[str, int], radius: int, start_dir_coord: tp.Tuple[int, int], \
                                 end_dir_coord: tp.Tuple[int, int], clockwise: bool, \
                                 node_specs: tp.List[tp.Optional[NodeSpec]] \
                                 ) -> tp.List[int]:

        parent_id = self._id_if_str(parent_id)
        num_specs = len(node_specs)
        if num_specs < 2:
            raise ValueError("node_specs must have at least 2 elements")
        if node_specs[0] is None or node_specs[-1] is None:
            raise ValueError(
                "The first and last item of node_specs must not be None")

        added_ids = []
        parent_pos = self._nodes[parent_id].pos
        parent_vec2 = angles.vec2(parent_pos)

        start_vec2 = angles.vec2(start_dir_coord) - parent_vec2
        end_vec2 = angles.vec2(end_dir_coord) - parent_vec2

        start_bear_rad = angles.get_bearing_rad_of(angles.flip_y(start_vec2))
        end_bear_rad = angles.get_bearing_rad_of(angles.flip_y(end_vec2))
        bear_diff_rad = angles.normalise_angle(end_bear_rad - start_bear_rad)
        if clockwise:
            bear_diff_rad = angles.flip_angle(bear_diff_rad)

        count = 0
        for spec in node_specs:
            if spec is not None:
                rotate_anticlockwise_by = bear_diff_rad * count / (num_specs -
                                                                   1)
                if clockwise:
                    rotate_anticlockwise_by *= -1
                dir_vec = angles.flip_y( \
                            angles.get_unit_vector_after_rotating( \
                                angles.flip_y(start_vec2), rotate_anticlockwise_by ))
                pos = parent_pos + dir_vec * radius

                new_id = self.add_node(spec.text, pos, spec.node_col,
                                       spec.multibox)
                if spec.link_draw == ArrowDraw.BACK_ARROW:
                    self.add_link(new_id, parent_id, spec.link_col,
                                  ArrowDraw.FWD_ARROW, None)
                elif spec.link_draw != ArrowDraw.NO_LINK:
                    self.add_link(parent_id, new_id, spec.link_col,
                                  spec.link_draw, spec.link_2_col)

                added_ids.append(new_id)
            count += 1

        return added_ids
    def add_depth_line_of_linked_nodes(self, start_id: tp.Tuple[str, int], dir: tp.Tuple[int, int], \
                                       link_length: int, \
                                       node_specs: tp.List[tp.Optional[NodeSpec]] \
                                       ) -> tp.List[int]:

        added_ids = []
        start_id = self._id_if_str(start_id)
        start_pos = angles.vec2(self._nodes[start_id].pos)
        unit_dir = angles.unit(dir)

        count = 1
        from_id = start_id
        for spec in node_specs:
            if spec is not None:
                pos = start_pos + unit_dir * link_length * count
                new_id = self.add_node(spec.text, pos, spec.node_col,
                                       spec.multibox)

                if spec.link_draw == ArrowDraw.BACK_ARROW:
                    self.add_link(new_id, from_id, spec.link_col,
                                  ArrowDraw.FWD_ARROW, None)
                elif spec.link_draw != ArrowDraw.NO_LINK:
                    self.add_link(from_id, new_id, spec.link_col,
                                  spec.link_draw, spec.link_2_col)

                added_ids.append(new_id)
                from_id = new_id
            count += 1

        return added_ids
    def add_breadth_line_centered_on(self, parent_id: tp.Tuple[str, int], center_coord: tp.Tuple[int, int], \
                                     link_length: int, node_specs: tp.List[tp.Optional[NodeSpec]] \
                                     ) -> tp.List[int]:
        num_specs = len(node_specs)
        if num_specs < 2:
            raise ValueError("node_specs must have at least 2 elements")

        parent_pos = self.pos_of(parent_id)
        rel_vec2 = angles.vec2(center_coord) - parent_pos
        rotated_vec2 = angles.flip_y( \
            angles.rotate_vector_to_left_by_90_deg( \
                angles.flip_y( angles.unit(rel_vec2) )))

        half_total_length = link_length * float(num_specs - 1) / 2.0

        start_coord = center_coord + rotated_vec2 * half_total_length
        end_coord = center_coord - rotated_vec2 * half_total_length

        return self.add_breadth_line_of_sibling_nodes(parent_id, start_coord,
                                                      end_coord, node_specs)