Module sverchok.utils.turtle

Expand source code
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

from collections import defaultdict

from sverchok.utils.sv_logging import sv_logger


class Turtle(object):
    """
    Face walking turtle API.

    At each moment in time, a turtle stays on one of the mesh faces,
    and looks towards one of edges of the face:

        +---+---+---+
        |   |   |   |
        +---+---+---+
        |   | ^ |   |
        |   | @ |   |
        +---+---+---+
        |   |   |   |
        +---+---+---+

    Walking primitives are turn_next(), turn_prev() and click().
    Other methods are build from these primitives.
    One of the most used methods is step().

    For selecting faces, there are two sets of methods:

    1. select(), unselect() and toggle() to set selection state of the current face.
    2. start_selecting() and stop_selecting() to select all faces which the
       turtle is passing. The selection mask can be specified, to select, for example,
       each second face.

    This class also provides the API to "paint" on custom data layers of the faces.
    Three types of "paints" are supported: int, float and str.
    Each painting layer is identified by it's name. The turtle can paint on several
    layers at the same time.
    By default, there is only one painting layer, of type int, named Turtle.PAINT.
    One can add other painting layers by calling declare_painting_layer().
    NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e.
    right after constructor was called.
    NB 2: If you wish to use other custom data layers on the same bmesh, for purposes
    other than painting by turtle, then you have to create them BEFORE calling the Turtle
    constructor.

    One can mark some faces as obstacles. The turtle will pass through obstacles, if you
    say it to step() on it; but you can explicitly check if the turtle is going to
    enter the obstacle face with turtle.is_looking_at_obstacle, or check if the turtle
    is already at obstacle with turtle.is_at_obstacle.
    """

    PREVIOUS = 'PREVIOUS'
    NEXT = 'NEXT'

    SELECT = 'SELECT'
    UNSELECT = 'UNSELECT'
    TOGGLE = 'TOGGLE'
    MASK = 'MASK'

    PAINT = 'turtle_paint'

    def __init__(self, bm, bm_face = None):
        self.bmesh = bm
        # Creation of the custom data layer invalidates all
        # references to mesh's BMFaces!
        self.index_layer = bm.faces.layers.int.new("turtle_index")
        self.obstacle_layer = bm.faces.layers.int.new("turtle_obstacle")
        bm.faces.ensure_lookup_table()
        bm.faces.index_update()
        self.current_face = bm.faces[0] if bm_face is None else bm_face
        self.current_loop = self.current_face.loops[0]
        self.opposite_bias = self.PREVIOUS
        self.selection_mode = None
        self.selection_mask = None
        self.selection_cycle_index = 0
        self.current_index = 1

        self.painting_layer = dict()
        self.painting_mask = dict()
        self.painting_index = defaultdict(int)
        self.is_painting = False

        self.current_face[self.index_layer] = self.current_index
        self.declare_painting_layer(self.PAINT)

    def declare_painting_layer(self, layer_name, data_type = int):
        """
        Create a custom data layer for painting.

        NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e.
        right after constructor was called.

        NB 2: If you wish to use other custom data layers on the same bmesh, for purposes
        other than painting by turtle, then you have to create them BEFORE calling the Turtle
        constructor.
        """
        if data_type == int:
            layers = self.bmesh.faces.layers.int
        elif data_type == float:
            layers = self.bmesh.faces.layers.float
        elif data_type == str:
            layers = self.bmesh.faces.layers.string

        # Creation of the custom data layer invalidates all
        # references to mesh's BMFaces!
        face_index = self.current_face.index
        loop_index = self.current_loop.index
        layer = layers.new(layer_name)
        self.bmesh.faces.ensure_lookup_table()
        self.bmesh.faces.index_update()
        self.painting_layer[layer_name] = layer
        self.current_face = self.bmesh.faces[face_index]
        self.current_loop = self.current_face.loops[loop_index]

    def turn_next(self, count=1):
        """
        Turn towards the next edge in the sequence.
        If face normal is oriented "as usual", then this
        means "turn counterclockwise".

            +----+      +----+
            |  ^ |  --> |    |
            |  @ |      | <@ |
            +----+      +----+

        If count is more than 1, then repeat this turn specified
        number of times.
        """
        for i in range(count):
            self.current_loop = self.current_loop.link_loop_next

    def turn_prev(self, count=1):
        """
        Turn towards the previous edge in the sequence.
        If face normal is oriented "as usual", then this
        means "turn clockwise".

            +----+      +----+
            | ^  |  --> |    |
            | @  |      | @> |
            +----+      +----+

        If count is more than 1, then repeat this turn specified
        number of times.
        """
        for i in range(count):
            self.current_loop = self.current_loop.link_loop_prev

    def click(self):
        """
        Jump to the face which is beyond the edge
        at which the turtle is looking currently.
        This turtle is a bit strange, because when it jumps it
        turns around, to look at the same edge it was looking, but
        from another side:

            +----+----+      +----+----+
            | @> |    |  --> |    | <@ |
            +----+----+      +----+----+

        This changes the selection state of the face where the turtle
        stepped, if it is in "start_selecting()" mode, according to selection
        mask.

        This updates custom data layers of the face where the turtle stepped,
        if it is in "start_painting()" mode, according to painting masks.
        """
        self.current_index += 1
        self.current_face[self.index_layer] = self.current_index
        next_loop = self.current_loop.link_loop_radial_next
        self.current_loop = next_loop
        self.current_face = next_loop.face

        sv_logger.debug("Current face # := %s", self.current_face.index)

        if self.selection_mode == self.MASK:
            if not self.selection_mask:
                raise Exception("Selection mode is set to MASK, but mask is not specified")
            n = len(self.selection_mask)
            self.selection_cycle_index = (self.selection_cycle_index + 1) % n
            mode = self.selection_mask[self.selection_cycle_index]
            if mode not in [self.SELECT, self.UNSELECT, self.TOGGLE, 0, 1, False, True]:
                raise Exception("Unsupported flag in the selection mask")
            if mode == True or mode == 1:
                mode = self.SELECT
            elif mode == False or mode == 0:
                mode = self.UNSELECT
        else:
            mode = self.selection_mode
        if mode == self.SELECT:
            self.select()
        elif mode == self.UNSELECT:
            self.unselect()
        elif mode == self.TOGGLE:
            self.toggle()

        if self.is_painting:
            for painting_layer in self.painting_layer.values():
                painting_mask = self.painting_mask.get(painting_layer.name)
                if not painting_mask:
                    raise Exception("Painting layer is set, but painting mask is not")
                n = len(painting_mask)
                self.painting_index[painting_layer.name] = (self.painting_index[painting_layer.name] + 1) % n
                value = painting_mask[self.painting_index[painting_layer.name]]
                self.current_face[painting_layer] = value
                sv_logger.debug("Paint face #%s, layer `%s' with value `%s'", self.current_face.index, painting_layer.name, value)

    def get_opposite_loop(self, loop, bias=None):
        """
        Get the BMLoop opposite to the current one.
        This does not change current turtle or mesh state.
        """
        if bias is None:
            bias = self.opposite_bias
        face = loop.face
        n = len(face.loops)
        if n % 2 == 0:
            steps = n // 2
        else:
            if bias == self.PREVIOUS:
                steps = n // 2
            else:
                steps = n // 2 + 1
        for i in range(steps):
            loop = loop.link_loop_next
        return loop

    def turn_opposite(self, bias=None):
        """
        Turn the turtle around, to look at the opposite edge:

            +----+       +----+
            | @> |  -->  | <@ |
            +----+       +----+

        If the current face has odd count of edges, then the term
        "around" is ambiguous:

                        +----+
                        |   > \
                        |  @   *
                        |     /
                      > +----+
                     /
            +----+  /
            |     \
            | <@   *   ? OR ?
            |     / 
            +----+  \
                     \
                      > +----+
                        |     \
                        |  @   *
                        |   > /
                        +----+

        To decide in such cases, there is `bias` parameter; it can have
        one of two values: Turtle.PREVIOUS and Turtle.NEXT.
        The default value of `bias` parameter can be set as "turtle.opposite_bias".
        By default it is set to Turtle.PREVIOUS.
        """
        self.current_loop = self.get_opposite_loop(self.current_loop)

    def get_next_face(self, count=1, bias=None):
        """
        Get the face (BMFace) which is beyond that edge at which turtle is
        currently looking. If count is greater than 1, then look for the next
        face in the same direction.
        Bias can be provided for cases of odd count of edges in the face.
        This does not change turtle or face state.
        """
        face = self.current_face
        loop = self.current_loop
        for i in range(count):
            # click
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            # opposite
            loop = self.get_opposite_loop(next_loop, bias)
        return face

    def get_face_at_next(self):
        loop = self.current_loop.link_loop_next
        next_loop = loop.link_loop_radial_next
        return next_loop.face

    def get_face_at_prev(self):
        loop = self.current_loop.link_loop_prev
        next_loop = loop.link_loop_radial_next
        return next_loop.face

    def get_direct_distance_to_edge(self, check_loop, maximum = None, bias=None):
        """
        Find distance to some "good" edge, if the turtle will step forward
        each time.

        Args:
            check_loop: function taking BMLoop and returning boolean.
            maximum: maximum number of faces to check (to prevent possible infinite loop,
                        or too long paths). None means the total number of faces in the mesh.
            bias: bias for get_next_face().

        Returns:
            number of steps to reach a "good" edge, or None if such edge is too far.
        """
        if maximum is None:
            maximum = len(self.bmesh.faces)
        i = 0
        face = self.current_face
        loop = self.current_loop
        while True:
            if i > maximum:
                return None
            if check_loop(loop):
                return i
            # click
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            # opposite
            loop = self.get_opposite_loop(next_loop, bias)
            i += 1
        return None
    
    def get_direct_distance_to_boundary(self, maximum = None, bias = None):
        """
        Find distance to a boundary edge, if the turtle will step forward
        each time.

        Args:
            maximum: maximum number of faces to check (to prevent possible infinite loop,
                        or too long paths). None means the total number of faces in the mesh.
            bias: bias for get_next_face().

        Returns:
            number of steps to reach a boundary edge, or None if such edge is too far.
        """
        return self.get_direct_distance_to_edge(lambda loop: loop.edge.is_boundary, maximum, bias)

    def get_direct_distance_to_obstacle(self, maximum = None, bias = None):
        """
        Find distance to an edge near an obstacle, if the turtle will step forward
        each time.

        Args:
            maximum: maximum number of faces to check (to prevent possible infinite loop,
                        or too long paths). None means the total number of faces in the mesh.
            bias: bias for get_next_face().

        Returns:
            number of steps to reach an edge of an obstacle, or None if such edge is too far.
        """
        if maximum is None:
            maximum = len(self.bmesh.faces)
        i = 0
        face = self.current_face
        loop = self.current_loop
        while True:
            if i > maximum:
                return None
            # click
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            if face[self.obstacle_layer]:
                return i
            # opposite
            loop = self.get_opposite_loop(next_loop, bias)
            i += 1
        return None

    def step(self, count=1, bias=None, stop_at_boundary = False, stop_at_obstacle = False):
        """
        Step to the next face, i.e. the face which is beyond the edge
        at which the turtle is currently looking, without changing 
        turtle's orientation.

            +----+----+      +----+----+
            | @> |    |  --> |    | @> |
            +----+----+      +----+----+

        If count is greater than 1, then repeat this step the specified
        number of times.
        Bias can be provided for cases of odd count of edges in the face.
        """
        for i in range(count):
            self.click()
            self.turn_opposite(bias=bias)
            if stop_at_boundary:
                if self.is_looking_at_boundary:
                    break
            if stop_at_obstacle:
                if self.is_looking_at_obstacle:
                    break

    def step_back(self, count=1, bias=None):
        """
        Similar to step(), but step backwards:

            +----+----+      +----+----+
            |    | @> |  --> | @> |    |
            +----+----+      +----+----+
        """
        for i in range(count):
            self.turn_opposite(bias=bias)
            self.click()

    def strafe_next(self, count=1, stop_at_boundary = False, stop_at_obstacle = False):
        """
        Step to the face which is in the "next" (i.e. usually counterclockwise)
        direction, without changing turtle's orientation:

            +----+----+      +----+----+
            |    |  ^ |      |  ^ |    |
            |    |  @ |  --> |  @ |    |
            +----+----+      +----+----+

        If count is greater than 1, then repeat this step the specified
        number of times.
        """
        for i in range(count):
            self.turn_next()
            if stop_at_boundary:
                if self.is_looking_at_boundary:
                    self.turn_prev()
                    break
            if stop_at_obstacle:
                if self.is_looking_at_obstacle:
                    self.turn_prev()
                    break
            self.click()
            self.turn_next()

    def strafe_prev(self, count=1, stop_at_boundary = False, stop_at_obstacle = False):
        """
        Step to the face which is in the "prev" (i.e. usually clockwise)
        direction, without changing turtle's orientation:

            +----+----+      +----+----+
            |  ^ |    |      |    |  ^ |
            |  @ |    |  --> |    |  @ |
            +----+----+      +----+----+

        If count is greater than 1, then repeat this step the specified
        number of times.
        """
        for i in range(count):
            self.turn_prev()
            if stop_at_boundary:
                if self.is_looking_at_boundary:
                    self.turn_next()
                    break
            if stop_at_obstacle:
                if self.is_looking_at_obstacle:
                    self.turn_next()
                    break
            self.click()
            self.turn_prev()

    def zig_zag(self, steps=1, leg=0, turns=1):
        for i in range(steps):
            self.step(count=leg)
            self.click()
            self.turn_next(turns)
            self.step(count=leg)
            self.click()
            self.turn_prev(turns)

    def select(self):
        """
        Mark the current face as selected.
        """
        self.current_face.select = True
        sv_logger.debug("Selecting face #%s", self.current_face.index)

    def unselect(self):
        """
        Mark the current face as not selected.
        """
        self.current_face.select = False
        sv_logger.debug("Unselecting face #%s", self.current_face.index)

    def toggle(self):
        """
        Toggle the selection state of the current face.
        """
        self.current_face.select = not self.current_face.select
        sv_logger.debug("Set face #%s selection := %s", self.current_face.index, self.current_face.select)

    def start_selecting(self, mode = None, mask=None):
        """
        Start selecting faces which the turtle passes.
        This mode can be stopped by calling stop_selecting().

        Args:
            mode: selection mode. Can be Turtle.SELECT, Turtle.UNSELECT, Turtle.TOGGLE or Turtle.MASK.
            mask: selection mask. Used with mode == Turtle.MASK. The mask is used with
                  infinite repetition. For example:

                      turtle.start_selecting(mode = Turtle.MASK, mask = [0, 1])
                      turtle.step(6)
                      turtle.stop_selecting()

                  This will select each other face: 0[1]2[3]4[5].

        If mask is not specified, then the mask used with the previous start_selecting() call
        will be used. If the mask was never provided, there will be an exception.
        """
        if mode is None:
            mode = self.SELECT
        self.selection_mode = mode
        if mode == self.MASK:
            if not mask and not self.selection_mask:
                raise Exception("You have to specify the mask when setting selection mode to MASK")
            if mask:
                self.selection_mask = mask

    def stop_selecting(self):
        """
        Stop selecting faces which the turtle passes.
        """
        self.selection_mode = None

    def paint(self, value, layer_name = PAINT):
        """
        Paint the current face with specified value, on specified layer.
        The painting layer must be defined by calling declare_painting_layer()
        right after calling the Turtle() constructor. By default, there is
        one painting layer, named Turtle.PAINT.
        """
        layer = self.painting_layer[layer_name]
        self.current_face[layer] = value

    def start_painting(self, value = None, layer_name = PAINT):
        """
        Start painting faces which the turtle passes.
        If value is not specified, then the value used with previous
        start_painting() call will be used. If the value was never provided,
        there will be an exception.

        A list of values can be specified instead of the single value. In this
        case, these values will be used in order, with repetition.
        The painting layer must be defined by calling declare_painting_layer()
        right after calling the Turtle() constructor. By default, there is
        one painting layer, named Turtle.PAINT.
        """
        if value is not None and not isinstance(value, (list, tuple)):
            value = [value]
        layer = self.painting_layer.get(layer_name)
        if layer is None:
            raise Exception("This layer was not declared")
        if value is None and self.painting_mask.get(layer_name) is None:
            raise Exception("Painting mask was not specified")
        if value is not None:
            self.painting_mask[layer_name] = value
        self.is_painting = True

    def stop_painting(self, layer_name = PAINT):
        """
        Stop painting faces which the turtle passes.
        """
        self.is_painting = False

    def reset_selection_cycle(self):
        self.selection_cycle_index = 0

    def reset_painting_cycle(self, layer_name = PAINT):
        self.painting_index[layer_name] = 0

    def set_is_obstacle(self, is_obstacle):
        """
        Mark current face as an obstacle.
        "Obstacle" is just a boolean indicator, that can be
        checked with turtle.get_is_obstacle or turtle.is_looking_at_obstacle.

        Args:
            is_obstacle : boolean or int, 1 or True for marking face as obstacle.
        """
        self.current_face[self.obstacle_layer] = int(is_obstacle)

    def get_is_obstacle(self, face):
        return bool(face[self.obstacle_layer])

    @property
    def is_at_obstacle(self):
        """
        Contains True if the turtle is currently at face which was marked
        as an obstacle.
        """
        return bool(self.current_face[self.obstacle_layer])

    def set_obstacle_mask(self, face_mask, invert=False):
        """
        Set obstacle indicators for all faces of the mesh.

        Args:
            face_mask: list of booleans or ints, True or 1 to mark face as an obstacle.
            invert: boolean, default false: set to True to invert the meaning of face_mask.
        """
        for is_obstacle, face in zip(face_mask, self.bmesh.faces):
            if invert:
                face[self.obstacle_layer] = 1 - int(is_obstacle)
            else:
                face[self.obstacle_layer] = int(is_obstacle)

    @property
    def is_looking_at_obstacle(self):
        """
        Contains True if the face beyond the edge at which the turtle is currently
        looking was marked as an obstacle.
        """
        face = self.get_next_face()
        return bool(face[self.obstacle_layer])

    def get_selected_faces_pydata(self):
        """
        Return list of selected faces in Sverchok format.
        """
        return [[vert.index for vert in face.verts] for face in self.bmesh.faces if face.select]

    def get_selected_faces(self):
        """
        Return list of selected faces.
        returns: list of BMFace.
        """
        return [face for face in self.bmesh.faces if face.select]

    def get_selection_mask(self):
        """
        Returns selection mask.
        result: list of booleans.
        """
        return [face.select for face in self.bmesh.faces]

    def get_painting_value(self, face, layer_name = PAINT):
        layer = self.painting_layer[layer_name]
        return face[layer]

    def get_painting_data(self, layer_name = PAINT):
        layer = self.painting_layer[layer_name]
        return [face[layer] for face in self.bmesh.faces]

    def get_is_looking_at_face_ring(self, max_length = None, consider_obstacles = True, bias = None):
        if max_length is None:
            max_length = len(self.bmesh.faces)
        loop = self.current_loop
        face = self.current_face
        i = 0
        while True:
            if i > max_length:
                return False
            i += 1
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            if consider_obstacles and face[self.obstacle_layer]:
                return False
            loop = self.get_opposite_loop(next_loop, bias)
            if loop.edge.is_boundary:
                return False
            if face == self.current_face:
                return True
        return False

    def was_at_face(self, face):
        """
        Whether the turtle already has previously visited the given face.

        Args:
            face : BMFace

        Returns:
            boolean
        """
        v = face[self.index_layer]
        return (v != 0)

    @property
    def is_looking_at_boundary(self):
        """
        Contains True if the edge at which the turtle is currently looking
        is a boundary edge.
        """
        return self.current_loop.edge.is_boundary

    @property
    def is_at_boundary(self):
        """
        Contains True if the current face is a boundary face.
        """
        return any(edge.is_boundary for edge in self.current_face.edges)

    @property
    def was_here(self):
        """
        Contains True if the turtle has previously visited the current face.
        """
        return self.was_at_face(self.current_face)

Classes

class Turtle (bm, bm_face=None)

Face walking turtle API.

At each moment in time, a turtle stays on one of the mesh faces, and looks towards one of edges of the face:

+---+---+---+
|   |   |   |
+---+---+---+
|   | ^ |   |
|   | @ |   |
+---+---+---+
|   |   |   |
+---+---+---+

Walking primitives are turn_next(), turn_prev() and click(). Other methods are build from these primitives. One of the most used methods is step().

For selecting faces, there are two sets of methods:

  1. select(), unselect() and toggle() to set selection state of the current face.
  2. start_selecting() and stop_selecting() to select all faces which the turtle is passing. The selection mask can be specified, to select, for example, each second face.

This class also provides the API to "paint" on custom data layers of the faces. Three types of "paints" are supported: int, float and str. Each painting layer is identified by it's name. The turtle can paint on several layers at the same time. By default, there is only one painting layer, of type int, named Turtle.PAINT. One can add other painting layers by calling declare_painting_layer(). NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e. right after constructor was called. NB 2: If you wish to use other custom data layers on the same bmesh, for purposes other than painting by turtle, then you have to create them BEFORE calling the Turtle constructor.

One can mark some faces as obstacles. The turtle will pass through obstacles, if you say it to step() on it; but you can explicitly check if the turtle is going to enter the obstacle face with turtle.is_looking_at_obstacle, or check if the turtle is already at obstacle with turtle.is_at_obstacle.

Expand source code
class Turtle(object):
    """
    Face walking turtle API.

    At each moment in time, a turtle stays on one of the mesh faces,
    and looks towards one of edges of the face:

        +---+---+---+
        |   |   |   |
        +---+---+---+
        |   | ^ |   |
        |   | @ |   |
        +---+---+---+
        |   |   |   |
        +---+---+---+

    Walking primitives are turn_next(), turn_prev() and click().
    Other methods are build from these primitives.
    One of the most used methods is step().

    For selecting faces, there are two sets of methods:

    1. select(), unselect() and toggle() to set selection state of the current face.
    2. start_selecting() and stop_selecting() to select all faces which the
       turtle is passing. The selection mask can be specified, to select, for example,
       each second face.

    This class also provides the API to "paint" on custom data layers of the faces.
    Three types of "paints" are supported: int, float and str.
    Each painting layer is identified by it's name. The turtle can paint on several
    layers at the same time.
    By default, there is only one painting layer, of type int, named Turtle.PAINT.
    One can add other painting layers by calling declare_painting_layer().
    NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e.
    right after constructor was called.
    NB 2: If you wish to use other custom data layers on the same bmesh, for purposes
    other than painting by turtle, then you have to create them BEFORE calling the Turtle
    constructor.

    One can mark some faces as obstacles. The turtle will pass through obstacles, if you
    say it to step() on it; but you can explicitly check if the turtle is going to
    enter the obstacle face with turtle.is_looking_at_obstacle, or check if the turtle
    is already at obstacle with turtle.is_at_obstacle.
    """

    PREVIOUS = 'PREVIOUS'
    NEXT = 'NEXT'

    SELECT = 'SELECT'
    UNSELECT = 'UNSELECT'
    TOGGLE = 'TOGGLE'
    MASK = 'MASK'

    PAINT = 'turtle_paint'

    def __init__(self, bm, bm_face = None):
        self.bmesh = bm
        # Creation of the custom data layer invalidates all
        # references to mesh's BMFaces!
        self.index_layer = bm.faces.layers.int.new("turtle_index")
        self.obstacle_layer = bm.faces.layers.int.new("turtle_obstacle")
        bm.faces.ensure_lookup_table()
        bm.faces.index_update()
        self.current_face = bm.faces[0] if bm_face is None else bm_face
        self.current_loop = self.current_face.loops[0]
        self.opposite_bias = self.PREVIOUS
        self.selection_mode = None
        self.selection_mask = None
        self.selection_cycle_index = 0
        self.current_index = 1

        self.painting_layer = dict()
        self.painting_mask = dict()
        self.painting_index = defaultdict(int)
        self.is_painting = False

        self.current_face[self.index_layer] = self.current_index
        self.declare_painting_layer(self.PAINT)

    def declare_painting_layer(self, layer_name, data_type = int):
        """
        Create a custom data layer for painting.

        NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e.
        right after constructor was called.

        NB 2: If you wish to use other custom data layers on the same bmesh, for purposes
        other than painting by turtle, then you have to create them BEFORE calling the Turtle
        constructor.
        """
        if data_type == int:
            layers = self.bmesh.faces.layers.int
        elif data_type == float:
            layers = self.bmesh.faces.layers.float
        elif data_type == str:
            layers = self.bmesh.faces.layers.string

        # Creation of the custom data layer invalidates all
        # references to mesh's BMFaces!
        face_index = self.current_face.index
        loop_index = self.current_loop.index
        layer = layers.new(layer_name)
        self.bmesh.faces.ensure_lookup_table()
        self.bmesh.faces.index_update()
        self.painting_layer[layer_name] = layer
        self.current_face = self.bmesh.faces[face_index]
        self.current_loop = self.current_face.loops[loop_index]

    def turn_next(self, count=1):
        """
        Turn towards the next edge in the sequence.
        If face normal is oriented "as usual", then this
        means "turn counterclockwise".

            +----+      +----+
            |  ^ |  --> |    |
            |  @ |      | <@ |
            +----+      +----+

        If count is more than 1, then repeat this turn specified
        number of times.
        """
        for i in range(count):
            self.current_loop = self.current_loop.link_loop_next

    def turn_prev(self, count=1):
        """
        Turn towards the previous edge in the sequence.
        If face normal is oriented "as usual", then this
        means "turn clockwise".

            +----+      +----+
            | ^  |  --> |    |
            | @  |      | @> |
            +----+      +----+

        If count is more than 1, then repeat this turn specified
        number of times.
        """
        for i in range(count):
            self.current_loop = self.current_loop.link_loop_prev

    def click(self):
        """
        Jump to the face which is beyond the edge
        at which the turtle is looking currently.
        This turtle is a bit strange, because when it jumps it
        turns around, to look at the same edge it was looking, but
        from another side:

            +----+----+      +----+----+
            | @> |    |  --> |    | <@ |
            +----+----+      +----+----+

        This changes the selection state of the face where the turtle
        stepped, if it is in "start_selecting()" mode, according to selection
        mask.

        This updates custom data layers of the face where the turtle stepped,
        if it is in "start_painting()" mode, according to painting masks.
        """
        self.current_index += 1
        self.current_face[self.index_layer] = self.current_index
        next_loop = self.current_loop.link_loop_radial_next
        self.current_loop = next_loop
        self.current_face = next_loop.face

        sv_logger.debug("Current face # := %s", self.current_face.index)

        if self.selection_mode == self.MASK:
            if not self.selection_mask:
                raise Exception("Selection mode is set to MASK, but mask is not specified")
            n = len(self.selection_mask)
            self.selection_cycle_index = (self.selection_cycle_index + 1) % n
            mode = self.selection_mask[self.selection_cycle_index]
            if mode not in [self.SELECT, self.UNSELECT, self.TOGGLE, 0, 1, False, True]:
                raise Exception("Unsupported flag in the selection mask")
            if mode == True or mode == 1:
                mode = self.SELECT
            elif mode == False or mode == 0:
                mode = self.UNSELECT
        else:
            mode = self.selection_mode
        if mode == self.SELECT:
            self.select()
        elif mode == self.UNSELECT:
            self.unselect()
        elif mode == self.TOGGLE:
            self.toggle()

        if self.is_painting:
            for painting_layer in self.painting_layer.values():
                painting_mask = self.painting_mask.get(painting_layer.name)
                if not painting_mask:
                    raise Exception("Painting layer is set, but painting mask is not")
                n = len(painting_mask)
                self.painting_index[painting_layer.name] = (self.painting_index[painting_layer.name] + 1) % n
                value = painting_mask[self.painting_index[painting_layer.name]]
                self.current_face[painting_layer] = value
                sv_logger.debug("Paint face #%s, layer `%s' with value `%s'", self.current_face.index, painting_layer.name, value)

    def get_opposite_loop(self, loop, bias=None):
        """
        Get the BMLoop opposite to the current one.
        This does not change current turtle or mesh state.
        """
        if bias is None:
            bias = self.opposite_bias
        face = loop.face
        n = len(face.loops)
        if n % 2 == 0:
            steps = n // 2
        else:
            if bias == self.PREVIOUS:
                steps = n // 2
            else:
                steps = n // 2 + 1
        for i in range(steps):
            loop = loop.link_loop_next
        return loop

    def turn_opposite(self, bias=None):
        """
        Turn the turtle around, to look at the opposite edge:

            +----+       +----+
            | @> |  -->  | <@ |
            +----+       +----+

        If the current face has odd count of edges, then the term
        "around" is ambiguous:

                        +----+
                        |   > \
                        |  @   *
                        |     /
                      > +----+
                     /
            +----+  /
            |     \
            | <@   *   ? OR ?
            |     / 
            +----+  \
                     \
                      > +----+
                        |     \
                        |  @   *
                        |   > /
                        +----+

        To decide in such cases, there is `bias` parameter; it can have
        one of two values: Turtle.PREVIOUS and Turtle.NEXT.
        The default value of `bias` parameter can be set as "turtle.opposite_bias".
        By default it is set to Turtle.PREVIOUS.
        """
        self.current_loop = self.get_opposite_loop(self.current_loop)

    def get_next_face(self, count=1, bias=None):
        """
        Get the face (BMFace) which is beyond that edge at which turtle is
        currently looking. If count is greater than 1, then look for the next
        face in the same direction.
        Bias can be provided for cases of odd count of edges in the face.
        This does not change turtle or face state.
        """
        face = self.current_face
        loop = self.current_loop
        for i in range(count):
            # click
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            # opposite
            loop = self.get_opposite_loop(next_loop, bias)
        return face

    def get_face_at_next(self):
        loop = self.current_loop.link_loop_next
        next_loop = loop.link_loop_radial_next
        return next_loop.face

    def get_face_at_prev(self):
        loop = self.current_loop.link_loop_prev
        next_loop = loop.link_loop_radial_next
        return next_loop.face

    def get_direct_distance_to_edge(self, check_loop, maximum = None, bias=None):
        """
        Find distance to some "good" edge, if the turtle will step forward
        each time.

        Args:
            check_loop: function taking BMLoop and returning boolean.
            maximum: maximum number of faces to check (to prevent possible infinite loop,
                        or too long paths). None means the total number of faces in the mesh.
            bias: bias for get_next_face().

        Returns:
            number of steps to reach a "good" edge, or None if such edge is too far.
        """
        if maximum is None:
            maximum = len(self.bmesh.faces)
        i = 0
        face = self.current_face
        loop = self.current_loop
        while True:
            if i > maximum:
                return None
            if check_loop(loop):
                return i
            # click
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            # opposite
            loop = self.get_opposite_loop(next_loop, bias)
            i += 1
        return None
    
    def get_direct_distance_to_boundary(self, maximum = None, bias = None):
        """
        Find distance to a boundary edge, if the turtle will step forward
        each time.

        Args:
            maximum: maximum number of faces to check (to prevent possible infinite loop,
                        or too long paths). None means the total number of faces in the mesh.
            bias: bias for get_next_face().

        Returns:
            number of steps to reach a boundary edge, or None if such edge is too far.
        """
        return self.get_direct_distance_to_edge(lambda loop: loop.edge.is_boundary, maximum, bias)

    def get_direct_distance_to_obstacle(self, maximum = None, bias = None):
        """
        Find distance to an edge near an obstacle, if the turtle will step forward
        each time.

        Args:
            maximum: maximum number of faces to check (to prevent possible infinite loop,
                        or too long paths). None means the total number of faces in the mesh.
            bias: bias for get_next_face().

        Returns:
            number of steps to reach an edge of an obstacle, or None if such edge is too far.
        """
        if maximum is None:
            maximum = len(self.bmesh.faces)
        i = 0
        face = self.current_face
        loop = self.current_loop
        while True:
            if i > maximum:
                return None
            # click
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            if face[self.obstacle_layer]:
                return i
            # opposite
            loop = self.get_opposite_loop(next_loop, bias)
            i += 1
        return None

    def step(self, count=1, bias=None, stop_at_boundary = False, stop_at_obstacle = False):
        """
        Step to the next face, i.e. the face which is beyond the edge
        at which the turtle is currently looking, without changing 
        turtle's orientation.

            +----+----+      +----+----+
            | @> |    |  --> |    | @> |
            +----+----+      +----+----+

        If count is greater than 1, then repeat this step the specified
        number of times.
        Bias can be provided for cases of odd count of edges in the face.
        """
        for i in range(count):
            self.click()
            self.turn_opposite(bias=bias)
            if stop_at_boundary:
                if self.is_looking_at_boundary:
                    break
            if stop_at_obstacle:
                if self.is_looking_at_obstacle:
                    break

    def step_back(self, count=1, bias=None):
        """
        Similar to step(), but step backwards:

            +----+----+      +----+----+
            |    | @> |  --> | @> |    |
            +----+----+      +----+----+
        """
        for i in range(count):
            self.turn_opposite(bias=bias)
            self.click()

    def strafe_next(self, count=1, stop_at_boundary = False, stop_at_obstacle = False):
        """
        Step to the face which is in the "next" (i.e. usually counterclockwise)
        direction, without changing turtle's orientation:

            +----+----+      +----+----+
            |    |  ^ |      |  ^ |    |
            |    |  @ |  --> |  @ |    |
            +----+----+      +----+----+

        If count is greater than 1, then repeat this step the specified
        number of times.
        """
        for i in range(count):
            self.turn_next()
            if stop_at_boundary:
                if self.is_looking_at_boundary:
                    self.turn_prev()
                    break
            if stop_at_obstacle:
                if self.is_looking_at_obstacle:
                    self.turn_prev()
                    break
            self.click()
            self.turn_next()

    def strafe_prev(self, count=1, stop_at_boundary = False, stop_at_obstacle = False):
        """
        Step to the face which is in the "prev" (i.e. usually clockwise)
        direction, without changing turtle's orientation:

            +----+----+      +----+----+
            |  ^ |    |      |    |  ^ |
            |  @ |    |  --> |    |  @ |
            +----+----+      +----+----+

        If count is greater than 1, then repeat this step the specified
        number of times.
        """
        for i in range(count):
            self.turn_prev()
            if stop_at_boundary:
                if self.is_looking_at_boundary:
                    self.turn_next()
                    break
            if stop_at_obstacle:
                if self.is_looking_at_obstacle:
                    self.turn_next()
                    break
            self.click()
            self.turn_prev()

    def zig_zag(self, steps=1, leg=0, turns=1):
        for i in range(steps):
            self.step(count=leg)
            self.click()
            self.turn_next(turns)
            self.step(count=leg)
            self.click()
            self.turn_prev(turns)

    def select(self):
        """
        Mark the current face as selected.
        """
        self.current_face.select = True
        sv_logger.debug("Selecting face #%s", self.current_face.index)

    def unselect(self):
        """
        Mark the current face as not selected.
        """
        self.current_face.select = False
        sv_logger.debug("Unselecting face #%s", self.current_face.index)

    def toggle(self):
        """
        Toggle the selection state of the current face.
        """
        self.current_face.select = not self.current_face.select
        sv_logger.debug("Set face #%s selection := %s", self.current_face.index, self.current_face.select)

    def start_selecting(self, mode = None, mask=None):
        """
        Start selecting faces which the turtle passes.
        This mode can be stopped by calling stop_selecting().

        Args:
            mode: selection mode. Can be Turtle.SELECT, Turtle.UNSELECT, Turtle.TOGGLE or Turtle.MASK.
            mask: selection mask. Used with mode == Turtle.MASK. The mask is used with
                  infinite repetition. For example:

                      turtle.start_selecting(mode = Turtle.MASK, mask = [0, 1])
                      turtle.step(6)
                      turtle.stop_selecting()

                  This will select each other face: 0[1]2[3]4[5].

        If mask is not specified, then the mask used with the previous start_selecting() call
        will be used. If the mask was never provided, there will be an exception.
        """
        if mode is None:
            mode = self.SELECT
        self.selection_mode = mode
        if mode == self.MASK:
            if not mask and not self.selection_mask:
                raise Exception("You have to specify the mask when setting selection mode to MASK")
            if mask:
                self.selection_mask = mask

    def stop_selecting(self):
        """
        Stop selecting faces which the turtle passes.
        """
        self.selection_mode = None

    def paint(self, value, layer_name = PAINT):
        """
        Paint the current face with specified value, on specified layer.
        The painting layer must be defined by calling declare_painting_layer()
        right after calling the Turtle() constructor. By default, there is
        one painting layer, named Turtle.PAINT.
        """
        layer = self.painting_layer[layer_name]
        self.current_face[layer] = value

    def start_painting(self, value = None, layer_name = PAINT):
        """
        Start painting faces which the turtle passes.
        If value is not specified, then the value used with previous
        start_painting() call will be used. If the value was never provided,
        there will be an exception.

        A list of values can be specified instead of the single value. In this
        case, these values will be used in order, with repetition.
        The painting layer must be defined by calling declare_painting_layer()
        right after calling the Turtle() constructor. By default, there is
        one painting layer, named Turtle.PAINT.
        """
        if value is not None and not isinstance(value, (list, tuple)):
            value = [value]
        layer = self.painting_layer.get(layer_name)
        if layer is None:
            raise Exception("This layer was not declared")
        if value is None and self.painting_mask.get(layer_name) is None:
            raise Exception("Painting mask was not specified")
        if value is not None:
            self.painting_mask[layer_name] = value
        self.is_painting = True

    def stop_painting(self, layer_name = PAINT):
        """
        Stop painting faces which the turtle passes.
        """
        self.is_painting = False

    def reset_selection_cycle(self):
        self.selection_cycle_index = 0

    def reset_painting_cycle(self, layer_name = PAINT):
        self.painting_index[layer_name] = 0

    def set_is_obstacle(self, is_obstacle):
        """
        Mark current face as an obstacle.
        "Obstacle" is just a boolean indicator, that can be
        checked with turtle.get_is_obstacle or turtle.is_looking_at_obstacle.

        Args:
            is_obstacle : boolean or int, 1 or True for marking face as obstacle.
        """
        self.current_face[self.obstacle_layer] = int(is_obstacle)

    def get_is_obstacle(self, face):
        return bool(face[self.obstacle_layer])

    @property
    def is_at_obstacle(self):
        """
        Contains True if the turtle is currently at face which was marked
        as an obstacle.
        """
        return bool(self.current_face[self.obstacle_layer])

    def set_obstacle_mask(self, face_mask, invert=False):
        """
        Set obstacle indicators for all faces of the mesh.

        Args:
            face_mask: list of booleans or ints, True or 1 to mark face as an obstacle.
            invert: boolean, default false: set to True to invert the meaning of face_mask.
        """
        for is_obstacle, face in zip(face_mask, self.bmesh.faces):
            if invert:
                face[self.obstacle_layer] = 1 - int(is_obstacle)
            else:
                face[self.obstacle_layer] = int(is_obstacle)

    @property
    def is_looking_at_obstacle(self):
        """
        Contains True if the face beyond the edge at which the turtle is currently
        looking was marked as an obstacle.
        """
        face = self.get_next_face()
        return bool(face[self.obstacle_layer])

    def get_selected_faces_pydata(self):
        """
        Return list of selected faces in Sverchok format.
        """
        return [[vert.index for vert in face.verts] for face in self.bmesh.faces if face.select]

    def get_selected_faces(self):
        """
        Return list of selected faces.
        returns: list of BMFace.
        """
        return [face for face in self.bmesh.faces if face.select]

    def get_selection_mask(self):
        """
        Returns selection mask.
        result: list of booleans.
        """
        return [face.select for face in self.bmesh.faces]

    def get_painting_value(self, face, layer_name = PAINT):
        layer = self.painting_layer[layer_name]
        return face[layer]

    def get_painting_data(self, layer_name = PAINT):
        layer = self.painting_layer[layer_name]
        return [face[layer] for face in self.bmesh.faces]

    def get_is_looking_at_face_ring(self, max_length = None, consider_obstacles = True, bias = None):
        if max_length is None:
            max_length = len(self.bmesh.faces)
        loop = self.current_loop
        face = self.current_face
        i = 0
        while True:
            if i > max_length:
                return False
            i += 1
            next_loop = loop.link_loop_radial_next
            face = next_loop.face
            if consider_obstacles and face[self.obstacle_layer]:
                return False
            loop = self.get_opposite_loop(next_loop, bias)
            if loop.edge.is_boundary:
                return False
            if face == self.current_face:
                return True
        return False

    def was_at_face(self, face):
        """
        Whether the turtle already has previously visited the given face.

        Args:
            face : BMFace

        Returns:
            boolean
        """
        v = face[self.index_layer]
        return (v != 0)

    @property
    def is_looking_at_boundary(self):
        """
        Contains True if the edge at which the turtle is currently looking
        is a boundary edge.
        """
        return self.current_loop.edge.is_boundary

    @property
    def is_at_boundary(self):
        """
        Contains True if the current face is a boundary face.
        """
        return any(edge.is_boundary for edge in self.current_face.edges)

    @property
    def was_here(self):
        """
        Contains True if the turtle has previously visited the current face.
        """
        return self.was_at_face(self.current_face)

Class variables

var MASK
var NEXT
var PAINT
var PREVIOUS
var SELECT
var TOGGLE
var UNSELECT

Instance variables

var is_at_boundary

Contains True if the current face is a boundary face.

Expand source code
@property
def is_at_boundary(self):
    """
    Contains True if the current face is a boundary face.
    """
    return any(edge.is_boundary for edge in self.current_face.edges)
var is_at_obstacle

Contains True if the turtle is currently at face which was marked as an obstacle.

Expand source code
@property
def is_at_obstacle(self):
    """
    Contains True if the turtle is currently at face which was marked
    as an obstacle.
    """
    return bool(self.current_face[self.obstacle_layer])
var is_looking_at_boundary

Contains True if the edge at which the turtle is currently looking is a boundary edge.

Expand source code
@property
def is_looking_at_boundary(self):
    """
    Contains True if the edge at which the turtle is currently looking
    is a boundary edge.
    """
    return self.current_loop.edge.is_boundary
var is_looking_at_obstacle

Contains True if the face beyond the edge at which the turtle is currently looking was marked as an obstacle.

Expand source code
@property
def is_looking_at_obstacle(self):
    """
    Contains True if the face beyond the edge at which the turtle is currently
    looking was marked as an obstacle.
    """
    face = self.get_next_face()
    return bool(face[self.obstacle_layer])
var was_here

Contains True if the turtle has previously visited the current face.

Expand source code
@property
def was_here(self):
    """
    Contains True if the turtle has previously visited the current face.
    """
    return self.was_at_face(self.current_face)

Methods

def click(self)

Jump to the face which is beyond the edge at which the turtle is looking currently. This turtle is a bit strange, because when it jumps it turns around, to look at the same edge it was looking, but from another side:

+----+----+      +----+----+
| @> |    |  --> |    | <@ |
+----+----+      +----+----+

This changes the selection state of the face where the turtle stepped, if it is in "start_selecting()" mode, according to selection mask.

This updates custom data layers of the face where the turtle stepped, if it is in "start_painting()" mode, according to painting masks.

Expand source code
def click(self):
    """
    Jump to the face which is beyond the edge
    at which the turtle is looking currently.
    This turtle is a bit strange, because when it jumps it
    turns around, to look at the same edge it was looking, but
    from another side:

        +----+----+      +----+----+
        | @> |    |  --> |    | <@ |
        +----+----+      +----+----+

    This changes the selection state of the face where the turtle
    stepped, if it is in "start_selecting()" mode, according to selection
    mask.

    This updates custom data layers of the face where the turtle stepped,
    if it is in "start_painting()" mode, according to painting masks.
    """
    self.current_index += 1
    self.current_face[self.index_layer] = self.current_index
    next_loop = self.current_loop.link_loop_radial_next
    self.current_loop = next_loop
    self.current_face = next_loop.face

    sv_logger.debug("Current face # := %s", self.current_face.index)

    if self.selection_mode == self.MASK:
        if not self.selection_mask:
            raise Exception("Selection mode is set to MASK, but mask is not specified")
        n = len(self.selection_mask)
        self.selection_cycle_index = (self.selection_cycle_index + 1) % n
        mode = self.selection_mask[self.selection_cycle_index]
        if mode not in [self.SELECT, self.UNSELECT, self.TOGGLE, 0, 1, False, True]:
            raise Exception("Unsupported flag in the selection mask")
        if mode == True or mode == 1:
            mode = self.SELECT
        elif mode == False or mode == 0:
            mode = self.UNSELECT
    else:
        mode = self.selection_mode
    if mode == self.SELECT:
        self.select()
    elif mode == self.UNSELECT:
        self.unselect()
    elif mode == self.TOGGLE:
        self.toggle()

    if self.is_painting:
        for painting_layer in self.painting_layer.values():
            painting_mask = self.painting_mask.get(painting_layer.name)
            if not painting_mask:
                raise Exception("Painting layer is set, but painting mask is not")
            n = len(painting_mask)
            self.painting_index[painting_layer.name] = (self.painting_index[painting_layer.name] + 1) % n
            value = painting_mask[self.painting_index[painting_layer.name]]
            self.current_face[painting_layer] = value
            sv_logger.debug("Paint face #%s, layer `%s' with value `%s'", self.current_face.index, painting_layer.name, value)
def declare_painting_layer(self, layer_name, data_type=builtins.int)

Create a custom data layer for painting.

NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e. right after constructor was called.

NB 2: If you wish to use other custom data layers on the same bmesh, for purposes other than painting by turtle, then you have to create them BEFORE calling the Turtle constructor.

Expand source code
def declare_painting_layer(self, layer_name, data_type = int):
    """
    Create a custom data layer for painting.

    NB 1: All painting layers must be declared BEFORE any commands to the turtle, i.e.
    right after constructor was called.

    NB 2: If you wish to use other custom data layers on the same bmesh, for purposes
    other than painting by turtle, then you have to create them BEFORE calling the Turtle
    constructor.
    """
    if data_type == int:
        layers = self.bmesh.faces.layers.int
    elif data_type == float:
        layers = self.bmesh.faces.layers.float
    elif data_type == str:
        layers = self.bmesh.faces.layers.string

    # Creation of the custom data layer invalidates all
    # references to mesh's BMFaces!
    face_index = self.current_face.index
    loop_index = self.current_loop.index
    layer = layers.new(layer_name)
    self.bmesh.faces.ensure_lookup_table()
    self.bmesh.faces.index_update()
    self.painting_layer[layer_name] = layer
    self.current_face = self.bmesh.faces[face_index]
    self.current_loop = self.current_face.loops[loop_index]
def get_direct_distance_to_boundary(self, maximum=None, bias=None)

Find distance to a boundary edge, if the turtle will step forward each time.

Args

maximum
maximum number of faces to check (to prevent possible infinite loop, or too long paths). None means the total number of faces in the mesh.
bias
bias for get_next_face().

Returns

number of steps to reach a boundary edge, or None if such edge is too far.

Expand source code
def get_direct_distance_to_boundary(self, maximum = None, bias = None):
    """
    Find distance to a boundary edge, if the turtle will step forward
    each time.

    Args:
        maximum: maximum number of faces to check (to prevent possible infinite loop,
                    or too long paths). None means the total number of faces in the mesh.
        bias: bias for get_next_face().

    Returns:
        number of steps to reach a boundary edge, or None if such edge is too far.
    """
    return self.get_direct_distance_to_edge(lambda loop: loop.edge.is_boundary, maximum, bias)
def get_direct_distance_to_edge(self, check_loop, maximum=None, bias=None)

Find distance to some "good" edge, if the turtle will step forward each time.

Args

check_loop
function taking BMLoop and returning boolean.
maximum
maximum number of faces to check (to prevent possible infinite loop, or too long paths). None means the total number of faces in the mesh.
bias
bias for get_next_face().

Returns

number of steps to reach a "good" edge, or None if such edge is too far.

Expand source code
def get_direct_distance_to_edge(self, check_loop, maximum = None, bias=None):
    """
    Find distance to some "good" edge, if the turtle will step forward
    each time.

    Args:
        check_loop: function taking BMLoop and returning boolean.
        maximum: maximum number of faces to check (to prevent possible infinite loop,
                    or too long paths). None means the total number of faces in the mesh.
        bias: bias for get_next_face().

    Returns:
        number of steps to reach a "good" edge, or None if such edge is too far.
    """
    if maximum is None:
        maximum = len(self.bmesh.faces)
    i = 0
    face = self.current_face
    loop = self.current_loop
    while True:
        if i > maximum:
            return None
        if check_loop(loop):
            return i
        # click
        next_loop = loop.link_loop_radial_next
        face = next_loop.face
        # opposite
        loop = self.get_opposite_loop(next_loop, bias)
        i += 1
    return None
def get_direct_distance_to_obstacle(self, maximum=None, bias=None)

Find distance to an edge near an obstacle, if the turtle will step forward each time.

Args

maximum
maximum number of faces to check (to prevent possible infinite loop, or too long paths). None means the total number of faces in the mesh.
bias
bias for get_next_face().

Returns

number of steps to reach an edge of an obstacle, or None if such edge is too far.

Expand source code
def get_direct_distance_to_obstacle(self, maximum = None, bias = None):
    """
    Find distance to an edge near an obstacle, if the turtle will step forward
    each time.

    Args:
        maximum: maximum number of faces to check (to prevent possible infinite loop,
                    or too long paths). None means the total number of faces in the mesh.
        bias: bias for get_next_face().

    Returns:
        number of steps to reach an edge of an obstacle, or None if such edge is too far.
    """
    if maximum is None:
        maximum = len(self.bmesh.faces)
    i = 0
    face = self.current_face
    loop = self.current_loop
    while True:
        if i > maximum:
            return None
        # click
        next_loop = loop.link_loop_radial_next
        face = next_loop.face
        if face[self.obstacle_layer]:
            return i
        # opposite
        loop = self.get_opposite_loop(next_loop, bias)
        i += 1
    return None
def get_face_at_next(self)
Expand source code
def get_face_at_next(self):
    loop = self.current_loop.link_loop_next
    next_loop = loop.link_loop_radial_next
    return next_loop.face
def get_face_at_prev(self)
Expand source code
def get_face_at_prev(self):
    loop = self.current_loop.link_loop_prev
    next_loop = loop.link_loop_radial_next
    return next_loop.face
def get_is_looking_at_face_ring(self, max_length=None, consider_obstacles=True, bias=None)
Expand source code
def get_is_looking_at_face_ring(self, max_length = None, consider_obstacles = True, bias = None):
    if max_length is None:
        max_length = len(self.bmesh.faces)
    loop = self.current_loop
    face = self.current_face
    i = 0
    while True:
        if i > max_length:
            return False
        i += 1
        next_loop = loop.link_loop_radial_next
        face = next_loop.face
        if consider_obstacles and face[self.obstacle_layer]:
            return False
        loop = self.get_opposite_loop(next_loop, bias)
        if loop.edge.is_boundary:
            return False
        if face == self.current_face:
            return True
    return False
def get_is_obstacle(self, face)
Expand source code
def get_is_obstacle(self, face):
    return bool(face[self.obstacle_layer])
def get_next_face(self, count=1, bias=None)

Get the face (BMFace) which is beyond that edge at which turtle is currently looking. If count is greater than 1, then look for the next face in the same direction. Bias can be provided for cases of odd count of edges in the face. This does not change turtle or face state.

Expand source code
def get_next_face(self, count=1, bias=None):
    """
    Get the face (BMFace) which is beyond that edge at which turtle is
    currently looking. If count is greater than 1, then look for the next
    face in the same direction.
    Bias can be provided for cases of odd count of edges in the face.
    This does not change turtle or face state.
    """
    face = self.current_face
    loop = self.current_loop
    for i in range(count):
        # click
        next_loop = loop.link_loop_radial_next
        face = next_loop.face
        # opposite
        loop = self.get_opposite_loop(next_loop, bias)
    return face
def get_opposite_loop(self, loop, bias=None)

Get the BMLoop opposite to the current one. This does not change current turtle or mesh state.

Expand source code
def get_opposite_loop(self, loop, bias=None):
    """
    Get the BMLoop opposite to the current one.
    This does not change current turtle or mesh state.
    """
    if bias is None:
        bias = self.opposite_bias
    face = loop.face
    n = len(face.loops)
    if n % 2 == 0:
        steps = n // 2
    else:
        if bias == self.PREVIOUS:
            steps = n // 2
        else:
            steps = n // 2 + 1
    for i in range(steps):
        loop = loop.link_loop_next
    return loop
def get_painting_data(self, layer_name='turtle_paint')
Expand source code
def get_painting_data(self, layer_name = PAINT):
    layer = self.painting_layer[layer_name]
    return [face[layer] for face in self.bmesh.faces]
def get_painting_value(self, face, layer_name='turtle_paint')
Expand source code
def get_painting_value(self, face, layer_name = PAINT):
    layer = self.painting_layer[layer_name]
    return face[layer]
def get_selected_faces(self)

Return list of selected faces. returns: list of BMFace.

Expand source code
def get_selected_faces(self):
    """
    Return list of selected faces.
    returns: list of BMFace.
    """
    return [face for face in self.bmesh.faces if face.select]
def get_selected_faces_pydata(self)

Return list of selected faces in Sverchok format.

Expand source code
def get_selected_faces_pydata(self):
    """
    Return list of selected faces in Sverchok format.
    """
    return [[vert.index for vert in face.verts] for face in self.bmesh.faces if face.select]
def get_selection_mask(self)

Returns selection mask. result: list of booleans.

Expand source code
def get_selection_mask(self):
    """
    Returns selection mask.
    result: list of booleans.
    """
    return [face.select for face in self.bmesh.faces]
def paint(self, value, layer_name='turtle_paint')

Paint the current face with specified value, on specified layer. The painting layer must be defined by calling declare_painting_layer() right after calling the Turtle() constructor. By default, there is one painting layer, named Turtle.PAINT.

Expand source code
def paint(self, value, layer_name = PAINT):
    """
    Paint the current face with specified value, on specified layer.
    The painting layer must be defined by calling declare_painting_layer()
    right after calling the Turtle() constructor. By default, there is
    one painting layer, named Turtle.PAINT.
    """
    layer = self.painting_layer[layer_name]
    self.current_face[layer] = value
def reset_painting_cycle(self, layer_name='turtle_paint')
Expand source code
def reset_painting_cycle(self, layer_name = PAINT):
    self.painting_index[layer_name] = 0
def reset_selection_cycle(self)
Expand source code
def reset_selection_cycle(self):
    self.selection_cycle_index = 0
def select(self)

Mark the current face as selected.

Expand source code
def select(self):
    """
    Mark the current face as selected.
    """
    self.current_face.select = True
    sv_logger.debug("Selecting face #%s", self.current_face.index)
def set_is_obstacle(self, is_obstacle)

Mark current face as an obstacle. "Obstacle" is just a boolean indicator, that can be checked with turtle.get_is_obstacle or turtle.is_looking_at_obstacle.

Args

is_obstacle : boolean or int, 1 or True for marking face as obstacle.

Expand source code
def set_is_obstacle(self, is_obstacle):
    """
    Mark current face as an obstacle.
    "Obstacle" is just a boolean indicator, that can be
    checked with turtle.get_is_obstacle or turtle.is_looking_at_obstacle.

    Args:
        is_obstacle : boolean or int, 1 or True for marking face as obstacle.
    """
    self.current_face[self.obstacle_layer] = int(is_obstacle)
def set_obstacle_mask(self, face_mask, invert=False)

Set obstacle indicators for all faces of the mesh.

Args

face_mask
list of booleans or ints, True or 1 to mark face as an obstacle.
invert
boolean, default false: set to True to invert the meaning of face_mask.
Expand source code
def set_obstacle_mask(self, face_mask, invert=False):
    """
    Set obstacle indicators for all faces of the mesh.

    Args:
        face_mask: list of booleans or ints, True or 1 to mark face as an obstacle.
        invert: boolean, default false: set to True to invert the meaning of face_mask.
    """
    for is_obstacle, face in zip(face_mask, self.bmesh.faces):
        if invert:
            face[self.obstacle_layer] = 1 - int(is_obstacle)
        else:
            face[self.obstacle_layer] = int(is_obstacle)
def start_painting(self, value=None, layer_name='turtle_paint')

Start painting faces which the turtle passes. If value is not specified, then the value used with previous start_painting() call will be used. If the value was never provided, there will be an exception.

A list of values can be specified instead of the single value. In this case, these values will be used in order, with repetition. The painting layer must be defined by calling declare_painting_layer() right after calling the Turtle() constructor. By default, there is one painting layer, named Turtle.PAINT.

Expand source code
def start_painting(self, value = None, layer_name = PAINT):
    """
    Start painting faces which the turtle passes.
    If value is not specified, then the value used with previous
    start_painting() call will be used. If the value was never provided,
    there will be an exception.

    A list of values can be specified instead of the single value. In this
    case, these values will be used in order, with repetition.
    The painting layer must be defined by calling declare_painting_layer()
    right after calling the Turtle() constructor. By default, there is
    one painting layer, named Turtle.PAINT.
    """
    if value is not None and not isinstance(value, (list, tuple)):
        value = [value]
    layer = self.painting_layer.get(layer_name)
    if layer is None:
        raise Exception("This layer was not declared")
    if value is None and self.painting_mask.get(layer_name) is None:
        raise Exception("Painting mask was not specified")
    if value is not None:
        self.painting_mask[layer_name] = value
    self.is_painting = True
def start_selecting(self, mode=None, mask=None)

Start selecting faces which the turtle passes. This mode can be stopped by calling stop_selecting().

Args

mode
selection mode. Can be Turtle.SELECT, Turtle.UNSELECT, Turtle.TOGGLE or Turtle.MASK.
mask

selection mask. Used with mode == Turtle.MASK. The mask is used with infinite repetition. For example:

  turtle.start_selecting(mode = Turtle.MASK, mask = [0, 1])
  turtle.step(6)
  turtle.stop_selecting()

This will select each other face: 0[1]2[3]4[5].

If mask is not specified, then the mask used with the previous start_selecting() call will be used. If the mask was never provided, there will be an exception.

Expand source code
def start_selecting(self, mode = None, mask=None):
    """
    Start selecting faces which the turtle passes.
    This mode can be stopped by calling stop_selecting().

    Args:
        mode: selection mode. Can be Turtle.SELECT, Turtle.UNSELECT, Turtle.TOGGLE or Turtle.MASK.
        mask: selection mask. Used with mode == Turtle.MASK. The mask is used with
              infinite repetition. For example:

                  turtle.start_selecting(mode = Turtle.MASK, mask = [0, 1])
                  turtle.step(6)
                  turtle.stop_selecting()

              This will select each other face: 0[1]2[3]4[5].

    If mask is not specified, then the mask used with the previous start_selecting() call
    will be used. If the mask was never provided, there will be an exception.
    """
    if mode is None:
        mode = self.SELECT
    self.selection_mode = mode
    if mode == self.MASK:
        if not mask and not self.selection_mask:
            raise Exception("You have to specify the mask when setting selection mode to MASK")
        if mask:
            self.selection_mask = mask
def step(self, count=1, bias=None, stop_at_boundary=False, stop_at_obstacle=False)

Step to the next face, i.e. the face which is beyond the edge at which the turtle is currently looking, without changing turtle's orientation.

+----+----+      +----+----+
| @> |    |  --> |    | @> |
+----+----+      +----+----+

If count is greater than 1, then repeat this step the specified number of times. Bias can be provided for cases of odd count of edges in the face.

Expand source code
def step(self, count=1, bias=None, stop_at_boundary = False, stop_at_obstacle = False):
    """
    Step to the next face, i.e. the face which is beyond the edge
    at which the turtle is currently looking, without changing 
    turtle's orientation.

        +----+----+      +----+----+
        | @> |    |  --> |    | @> |
        +----+----+      +----+----+

    If count is greater than 1, then repeat this step the specified
    number of times.
    Bias can be provided for cases of odd count of edges in the face.
    """
    for i in range(count):
        self.click()
        self.turn_opposite(bias=bias)
        if stop_at_boundary:
            if self.is_looking_at_boundary:
                break
        if stop_at_obstacle:
            if self.is_looking_at_obstacle:
                break
def step_back(self, count=1, bias=None)

Similar to step(), but step backwards:

+----+----+      +----+----+
|    | @> |  --> | @> |    |
+----+----+      +----+----+
Expand source code
def step_back(self, count=1, bias=None):
    """
    Similar to step(), but step backwards:

        +----+----+      +----+----+
        |    | @> |  --> | @> |    |
        +----+----+      +----+----+
    """
    for i in range(count):
        self.turn_opposite(bias=bias)
        self.click()
def stop_painting(self, layer_name='turtle_paint')

Stop painting faces which the turtle passes.

Expand source code
def stop_painting(self, layer_name = PAINT):
    """
    Stop painting faces which the turtle passes.
    """
    self.is_painting = False
def stop_selecting(self)

Stop selecting faces which the turtle passes.

Expand source code
def stop_selecting(self):
    """
    Stop selecting faces which the turtle passes.
    """
    self.selection_mode = None
def strafe_next(self, count=1, stop_at_boundary=False, stop_at_obstacle=False)

Step to the face which is in the "next" (i.e. usually counterclockwise) direction, without changing turtle's orientation:

+----+----+      +----+----+
|    |  ^ |      |  ^ |    |
|    |  @ |  --> |  @ |    |
+----+----+      +----+----+

If count is greater than 1, then repeat this step the specified number of times.

Expand source code
def strafe_next(self, count=1, stop_at_boundary = False, stop_at_obstacle = False):
    """
    Step to the face which is in the "next" (i.e. usually counterclockwise)
    direction, without changing turtle's orientation:

        +----+----+      +----+----+
        |    |  ^ |      |  ^ |    |
        |    |  @ |  --> |  @ |    |
        +----+----+      +----+----+

    If count is greater than 1, then repeat this step the specified
    number of times.
    """
    for i in range(count):
        self.turn_next()
        if stop_at_boundary:
            if self.is_looking_at_boundary:
                self.turn_prev()
                break
        if stop_at_obstacle:
            if self.is_looking_at_obstacle:
                self.turn_prev()
                break
        self.click()
        self.turn_next()
def strafe_prev(self, count=1, stop_at_boundary=False, stop_at_obstacle=False)

Step to the face which is in the "prev" (i.e. usually clockwise) direction, without changing turtle's orientation:

+----+----+      +----+----+
|  ^ |    |      |    |  ^ |
|  @ |    |  --> |    |  @ |
+----+----+      +----+----+

If count is greater than 1, then repeat this step the specified number of times.

Expand source code
def strafe_prev(self, count=1, stop_at_boundary = False, stop_at_obstacle = False):
    """
    Step to the face which is in the "prev" (i.e. usually clockwise)
    direction, without changing turtle's orientation:

        +----+----+      +----+----+
        |  ^ |    |      |    |  ^ |
        |  @ |    |  --> |    |  @ |
        +----+----+      +----+----+

    If count is greater than 1, then repeat this step the specified
    number of times.
    """
    for i in range(count):
        self.turn_prev()
        if stop_at_boundary:
            if self.is_looking_at_boundary:
                self.turn_next()
                break
        if stop_at_obstacle:
            if self.is_looking_at_obstacle:
                self.turn_next()
                break
        self.click()
        self.turn_prev()
def toggle(self)

Toggle the selection state of the current face.

Expand source code
def toggle(self):
    """
    Toggle the selection state of the current face.
    """
    self.current_face.select = not self.current_face.select
    sv_logger.debug("Set face #%s selection := %s", self.current_face.index, self.current_face.select)
def turn_next(self, count=1)

Turn towards the next edge in the sequence. If face normal is oriented "as usual", then this means "turn counterclockwise".

+----+      +----+
|  ^ |  --> |    |
|  @ |      | <@ |
+----+      +----+

If count is more than 1, then repeat this turn specified number of times.

Expand source code
def turn_next(self, count=1):
    """
    Turn towards the next edge in the sequence.
    If face normal is oriented "as usual", then this
    means "turn counterclockwise".

        +----+      +----+
        |  ^ |  --> |    |
        |  @ |      | <@ |
        +----+      +----+

    If count is more than 1, then repeat this turn specified
    number of times.
    """
    for i in range(count):
        self.current_loop = self.current_loop.link_loop_next
def turn_opposite(self, bias=None)

Turn the turtle around, to look at the opposite edge:

+----+       +----+
| @> |  -->  | <@ |
+----+       +----+

If the current face has odd count of edges, then the term "around" is ambiguous:

            +----+
            |   >                         |  @   *
            |     /
          > +----+
         /
+----+  /
|                 | <@   *   ? OR ?
|     / 
+----+                                             > +----+
            |                             |  @   *
            |   > /
            +----+

To decide in such cases, there is bias parameter; it can have one of two values: Turtle.PREVIOUS and Turtle.NEXT. The default value of bias parameter can be set as "turtle.opposite_bias". By default it is set to Turtle.PREVIOUS.

Expand source code
def turn_opposite(self, bias=None):
    """
    Turn the turtle around, to look at the opposite edge:

        +----+       +----+
        | @> |  -->  | <@ |
        +----+       +----+

    If the current face has odd count of edges, then the term
    "around" is ambiguous:

                    +----+
                    |   > \
                    |  @   *
                    |     /
                  > +----+
                 /
        +----+  /
        |     \
        | <@   *   ? OR ?
        |     / 
        +----+  \
                 \
                  > +----+
                    |     \
                    |  @   *
                    |   > /
                    +----+

    To decide in such cases, there is `bias` parameter; it can have
    one of two values: Turtle.PREVIOUS and Turtle.NEXT.
    The default value of `bias` parameter can be set as "turtle.opposite_bias".
    By default it is set to Turtle.PREVIOUS.
    """
    self.current_loop = self.get_opposite_loop(self.current_loop)
def turn_prev(self, count=1)

Turn towards the previous edge in the sequence. If face normal is oriented "as usual", then this means "turn clockwise".

+----+      +----+
| ^  |  --> |    |
| @  |      | @> |
+----+      +----+

If count is more than 1, then repeat this turn specified number of times.

Expand source code
def turn_prev(self, count=1):
    """
    Turn towards the previous edge in the sequence.
    If face normal is oriented "as usual", then this
    means "turn clockwise".

        +----+      +----+
        | ^  |  --> |    |
        | @  |      | @> |
        +----+      +----+

    If count is more than 1, then repeat this turn specified
    number of times.
    """
    for i in range(count):
        self.current_loop = self.current_loop.link_loop_prev
def unselect(self)

Mark the current face as not selected.

Expand source code
def unselect(self):
    """
    Mark the current face as not selected.
    """
    self.current_face.select = False
    sv_logger.debug("Unselecting face #%s", self.current_face.index)
def was_at_face(self, face)

Whether the turtle already has previously visited the given face.

Args

face : BMFace

Returns

boolean

Expand source code
def was_at_face(self, face):
    """
    Whether the turtle already has previously visited the given face.

    Args:
        face : BMFace

    Returns:
        boolean
    """
    v = face[self.index_layer]
    return (v != 0)
def zig_zag(self, steps=1, leg=0, turns=1)
Expand source code
def zig_zag(self, steps=1, leg=0, turns=1):
    for i in range(steps):
        self.step(count=leg)
        self.click()
        self.turn_next(turns)
        self.step(count=leg)
        self.click()
        self.turn_prev(turns)