def vert_quality(script, min_quality=0.0, max_quality=0.05, inclusive=True):
    """ Select all the faces and vertexes within the specified vertex quality
        range.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        min_quality (float): Minimum acceptable quality value.
        max_quality (float): Maximum acceptable quality value.
        inclusive (bool): If True only the faces with ALL the vertices within
            the specified range are selected. Otherwise any face with at least
            one vertex within the range is selected.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Select by Vertex Quality">\n',
        '    <Param name="minQ" ',
        'value="{}" '.format(min_quality),
        'description="Min Quality" ',
        'min="0" ',
        'max="{}" '.format(2 * max_quality),
        'type="RichDynamicFloat" ',
        '/>\n',
        '    <Param name="maxQ" ',
        'value="{}" '.format(max_quality),
        'description="Max Quality" ',
        'min="0" ',
        'max="{}" '.format(2 * max_quality),
        'type="RichDynamicFloat" ',
        '/>\n',
        '    <Param name="Inclusive" ',
        'value="{}" '.format(str(inclusive).lower()),
        'description="Inclusive Sel." ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Select all the faces and vertexes within the specified vertex quality range .


def face_function(script, function='(fi == 0)'):
    """Boolean function using muparser lib to perform face selection over
        current mesh.

    See help(mlx.muparser_ref) for muparser reference documentation.

    It's possible to use parenthesis, per-vertex variables and boolean operator:
        (, ), and, or, <, >, =
    It's possible to use per-face variables like attributes associated to the three
    vertices of every face.

    Variables (per face):
        x0, y0, z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex
        nx0, ny0, nz0, nx1, ny1, nz1, etc. for vertex normals
        r0, g0, b0, a0, etc. for vertex color
        q0, q1, q2 for quality
        wtu0, wtv0, wtu1, wtv1, wtu2, wtv2 (per wedge texture coordinates)
        ti for face texture index (>= ML2016.12)
        vsel0, vsel1, vsel2 for vertex selection (1 yes, 0 no) (>= ML2016.12)
        fr, fg, fb, fa for face color (>= ML2016.12)
        fq for face quality (>= ML2016.12)
        fnx, fny, fnz for face normal (>= ML2016.12)
        fsel face selection (1 yes, 0 no) (>= ML2016.12)

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        function (str): a boolean function that will be evaluated in order
            to select a subset of faces.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Conditional Face Selection">\n',
        '    <Param name="condSelect" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="boolean function" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Boolean function using muparser lib to perform face selection over current mesh .


def vert_function(script, function='(q < 0)', strict_face_select=True):
    """Boolean function using muparser lib to perform vertex selection over current mesh.

    See help(mlx.muparser_ref) for muparser reference documentation.

    It's possible to use parenthesis, per-vertex variables and boolean operator:
        (, ), and, or, <, >, =
    It's possible to use the following per-vertex variables in the expression:

    Variables:
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad
        vi (vertex index)
        vtu, vtv (texture coordinates)
        ti (texture index)
        vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        function (str): a boolean function that will be evaluated in order
            to select a subset of vertices. Example: (y > 0) and (ny > 0)
        strict_face_select (bool): if True a face is selected if ALL its
            vertices are selected. If False a face is selected if at least
            one of its vertices is selected. ML v1.3.4BETA only; this is
            ignored in 2016.12. In 2016.12 only vertices are selected.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if script.ml_version == '1.3.4BETA':
        strict_select = ''.join([
            '    <Param name="strictSelect" ',
            'value="{}" '.format(str(strict_face_select).lower()),
            'description="Strict face selection" ',
            'type="RichBool" ',
            '/>\n',
            ])
    else:
        strict_select = ''

    filter_xml = ''.join([
        '  <filter name="Conditional Vertex Selection">\n',
        '    <Param name="condSelect" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="boolean function" ',
        'type="RichString" ',
        '/>\n',
        strict_select,
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Boolean function using muparser lib to perform vertex selection over current mesh .


def cylindrical_vert(script, radius=1.0, inside=True):
    """Select all vertices within a cylindrical radius

    Args:
        radius (float): radius of the sphere
        center_pt (3 coordinate tuple or list): center point of the sphere

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if inside:
        function = 'sqrt(x^2+y^2)<={}'.format(radius)
    else:
        function = 'sqrt(x^2+y^2)>={}'.format(radius)
    vert_function(script, function=function)
    return None

def cylindrical_vert ( script , radius = 1.0 , inside = True ) : if inside : function = 'sqrt(x^2+y^2)<={}' . format ( radius ) else : function = 'sqrt(x^2+y^2)>={}' . format ( radius ) vert_function ( script , function = function ) return None


def spherical_vert(script, radius=1.0, center_pt=(0.0, 0.0, 0.0)):
    """Select all vertices within a spherical radius

    Args:
        radius (float): radius of the sphere
        center_pt (3 coordinate tuple or list): center point of the sphere

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    function = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)<={}'.format(
        center_pt[0], center_pt[1], center_pt[2], radius)
    vert_function(script, function=function)
    return None

def spherical_vert ( script , radius = 1.0 , center_pt = ( 0.0 , 0.0 , 0.0 ) ) : function = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)<={}' . format ( center_pt [ 0 ] , center_pt [ 1 ] , center_pt [ 2 ] , radius ) vert_function ( script , function = function ) return None


def join(script, merge_visible=True, merge_vert=False, delete_layer=True,
         keep_unreferenced_vert=False):
    """ Flatten all or only the visible layers into a single new mesh.

    Transformations are preserved. Existing layers can be optionally
    deleted.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        merge_visible (bool): merge only visible layers
        merge_vert (bool): merge the vertices that are duplicated among
            different layers. Very useful when the layers are spliced portions
            of a single big mesh.
        delete_layer (bool): delete all the merged layers. If all layers are
            visible only a single layer will remain after the invocation of
            this filter.
        keep_unreferenced_vert (bool): Do not discard unreferenced vertices
            from source layers. Necessary for point-only layers.

    Layer stack:
        Creates a new layer "Merged Mesh"
        Changes current layer to the new layer
        Optionally deletes all other layers

    MeshLab versions:
        2016.12
        1.3.4BETA

    Bugs:
        UV textures: not currently preserved, however will be in a future
            release. https://github.com/cnr-isti-vclab/meshlab/issues/128
        merge_visible: it is not currently possible to change the layer
            visibility from meshlabserver, however this will be possible
            in the future https://github.com/cnr-isti-vclab/meshlab/issues/123
    """
    filter_xml = ''.join([
        '  <filter name="Flatten Visible Layers">\n',
        '    <Param name="MergeVisible" ',
        'value="{}" '.format(str(merge_visible).lower()),
        'description="Merge Only Visible Layers" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="MergeVertices" ',
        'value="{}" '.format(str(merge_vert).lower()),
        'description="Merge duplicate vertices" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="DeleteLayer" ',
        'value="{}" '.format(str(delete_layer).lower()),
        'description="Delete Layers" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="AlsoUnreferenced" ',
        'value="{}" '.format(str(keep_unreferenced_vert).lower()),
        'description="Keep unreferenced vertices" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.add_layer('Merged Mesh')
        if delete_layer:
            # As it is not yet possible to change the layer visibility, all
            # layers will be deleted. This will be updated once layer
            # visibility is tracked.
            for i in range(script.last_layer()):
                script.del_layer(0)
    return None

Flatten all or only the visible layers into a single new mesh .


def rename(script, label='blank', layer_num=None):
    """ Rename layer label

    Can be useful for outputting mlp files, as the output file names use
    the labels.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        label (str): new label for the mesh layer
        layer_num (int): layer number to rename. Default is the
            current layer. Not supported on the file base API.

    Layer stack:
        Renames a layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Rename Current Mesh">\n',
        '    <Param name="newName" ',
        'value="{}" '.format(label),
        'description="New Label" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    if isinstance(script, mlx.FilterScript):
        if (layer_num is None) or (layer_num == script.current_layer()):
            util.write_filter(script, filter_xml)
            script.layer_stack[script.current_layer()] = label
        else:
            cur_layer = script.current_layer()
            change(script, layer_num)
            util.write_filter(script, filter_xml)
            change(script, cur_layer)
            script.layer_stack[layer_num] = label
    else:
        util.write_filter(script, filter_xml)
    return None


def change(script, layer_num=None):
    """ Change the current layer by specifying the new layer number.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        layer_num (int): the number of the layer to change to. Default is the
            last layer if script is a mlx.FilterScript object; if script is a
            filename the default is the first layer.

    Layer stack:
        Modifies current layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if layer_num is None:
        if isinstance(script, mlx.FilterScript):
            layer_num = script.last_layer()
        else:
            layer_num = 0
    filter_xml = ''.join([
        '  <filter name="Change the current layer">\n',
        '    <Param name="mesh" ',
        'value="{:d}" '.format(layer_num),
        'description="Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.set_current_layer(layer_num)
        #script.layer_stack[len(self.layer_stack) - 1] = layer_num
    return None

Change the current layer by specifying the new layer number .


def duplicate(script, layer_num=None):
    """ Duplicate a layer.

    New layer label is '*_copy'.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        layer_num (int): layer number to duplicate. Default is the
            current layer. Not supported on the file base API.

    Layer stack:
        Creates a new layer
        Changes current layer to the new layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = '  <filter name="Duplicate Current layer"/>\n'
    if isinstance(script, mlx.FilterScript):
        if (layer_num is None) or (layer_num == script.current_layer()):
            util.write_filter(script, filter_xml)
            script.add_layer('{}_copy'.format(script.layer_stack[script.current_layer()]), True)
        else:
            change(script, layer_num)
            util.write_filter(script, filter_xml)
            script.add_layer('{}_copy'.format(script.layer_stack[layer_num]), True)
    else:
        util.write_filter(script, filter_xml)
    return None


def delete_lower(script, layer_num=None):
    """ Delete all layers below the specified one.

    Useful for MeshLab ver 2016.12, whcih will only output layer 0.
    """
    if layer_num is None:
        layer_num = script.current_layer()
    if layer_num != 0:
        change(script, 0)
    for i in range(layer_num):
        delete(script, 0)
    return None

def delete_lower ( script , layer_num = None ) : if layer_num is None : layer_num = script . current_layer ( ) if layer_num != 0 : change ( script , 0 ) for i in range ( layer_num ) : delete ( script , 0 ) return None

Delete all layers below the specified one .


def handle_error(program_name, cmd, log=None):
    """Subprocess program error handling

    Args:
        program_name (str): name of the subprocess program

    Returns:
        break_now (bool): indicate whether calling program should break out of loop

    """
    print('\nHouston, we have a problem.',
          '\n%s did not finish successfully. Review the log' % program_name,
          'file and the input file(s) to see what went wrong.')
    print('%s command: "%s"' % (program_name, cmd))
    if log is not None:
        print('log: "%s"' % log)
    print('Where do we go from here?')
    print(' r  - retry running %s (probably after' % program_name,
          'you\'ve fixed any problems with the input files)')
    print(' c  - continue on with the script (probably after',
          'you\'ve manually re-run and generated the desired',
          'output file(s)')
    print(' x  - exit, keeping the TEMP3D files and log')
    print(' xd - exit, deleting the TEMP3D files and log')
    while True:
        choice = input('Select r, c, x (default), or xd: ')
        if choice not in ('r', 'c', 'x', 'xd'):
            #print('Please enter a valid option.')
            choice = 'x'
        #else:
        break
    if choice == 'x':
        print('Exiting ...')
        sys.exit(1)
    elif choice == 'xd':
        print('Deleting TEMP3D* and log files and exiting ...')
        util.delete_all('TEMP3D*')
        if log is not None:
            os.remove(log)
        sys.exit(1)
    elif choice == 'c':
        print('Continuing on ...')
        break_now = True
    elif choice == 'r':
        print('Retrying %s cmd ...' % program_name)
        break_now = False
    return break_now

' r  - retry running %s (probably after'


def begin(script='TEMP3D_default.mlx', file_in=None, mlp_in=None):
    """Create new mlx script and write opening tags.

    Performs special processing on stl files.

    If no input files are provided this will create a dummy
    file and delete it as the first filter. This works around
    the meshlab limitation that it must be provided an input
    file, even if you will be creating a mesh as the first
    filter.

    """
    script_file = open(script, 'w')
    script_file.write(''.join(['<!DOCTYPE FilterScript>\n',
                               '<FilterScript>\n']))
    script_file.close()

    current_layer = -1
    last_layer = -1
    stl = False

    # Process project files first
    if mlp_in is not None:
        # make a list if it isn't already
        if not isinstance(mlp_in, list):
            mlp_in = [mlp_in]
        for val in mlp_in:
            tree = ET.parse(val)
            #root = tree.getroot()
            for elem in tree.iter(tag='MLMesh'):
                filename = (elem.attrib['filename'])
                current_layer += 1
                last_layer += 1
                # If the mesh file extension is stl, change to that layer and
                # run clean.merge_vert
                if os.path.splitext(filename)[1][1:].strip().lower() == 'stl':
                    layers.change(script, current_layer)
                    clean.merge_vert(script)
                    stl = True

    # Process separate input files next
    if file_in is not None:
        # make a list if it isn't already
        if not isinstance(file_in, list):
            file_in = [file_in]
        for val in file_in:
            current_layer += 1
            last_layer += 1
            # If the mesh file extension is stl, change to that layer and
            # run clean.merge_vert
            if os.path.splitext(val)[1][1:].strip().lower() == 'stl':
                layers.change(script, current_layer)
                clean.merge_vert(script)
                stl = True

    # If some input files were stl, we need to change back to the last layer
    if stl:
        layers.change(script, last_layer)  # Change back to the last layer
    elif last_layer == -1:
        # If no input files are provided, create a dummy file
        # with a single vertex and delete it first in the script.
        # This works around the fact that meshlabserver will
        # not run without an input file.
        file_in = ['TEMP3D.xyz']
        file_in_descriptor = open(file_in[0], 'w')
        file_in_descriptor.write('0 0 0')
        file_in_descriptor.close()
        layers.delete(script)
    return current_layer, last_layer

def
begin
(
script
=
'TEMP3D_default.mlx'
,
file_in
=
,
mlp_in
=
)
:
script_file
=
open
(
script
,
'w'
)
script_file
.
write
(
''
.
join
(
[
'<!DOCTYPE FilterScript>\n'
,
'<FilterScript>\n'
]
)
)
script_file
.
close
(
)
current_layer
=
1
last_layer
=
1
stl
=
False
if
mlp_in
is
not
:
if
not
isinstance
(
mlp_in
,
list
)
:
mlp_in
=
[
mlp_in
]
for
val
in
mlp_in
:
tree
=
ET
.
parse
(
val
)
for
elem
in
tree
.
iter
(
tag
=
'MLMesh'
)
:
filename
=
(
elem
.
attrib
[
'filename'
]
)
current_layer
+=
1
last_layer
+=
1
if
os
.
path
.
splitext
(
filename
)
[
1
]
[
1
:
]
.
strip
(
)
.
lower
(
)
==
'stl'
:
layers
.
change
(
script
,
current_layer
)
clean
.
merge_vert
(
script
)
stl
=
True
if
file_in
is
not
:
if
not
isinstance
(
file_in
,
list
)
:
file_in
=
[
file_in
]
for
val
in
file_in
:
current_layer
+=
1
last_layer
+=
1
if
os
.
path
.
splitext
(
val
)
[
1
]
[
1
:
]
.
strip
(
)
.
lower
(
)
==
'stl'
:
layers
.
change
(
script
,
current_layer
)
clean
.
merge_vert
(
script
)
stl
=
True
if
stl
:
layers
.
change
(
script
,
last_layer
)
elif
last_layer
==
1
:
file_in
=
[
'TEMP3D.xyz'
]
file_in_descriptor
=
open
(
file_in
[
0
]
,
'w'
)
file_in_descriptor
.
write
(
'0 0 0'
)
file_in_descriptor
.
close
(
)
layers
.
delete
(
script
)
return
current_layer
,
last_layer

Create new mlx script and write opening tags .


def add_layer(self, label, change_layer=True):
        """ Add new mesh layer to the end of the stack

        Args:
            label (str): new label for the mesh layer
            change_layer (bool): change to the newly created layer
        """
        self.layer_stack.insert(self.last_layer() + 1, label)
        if change_layer:
            self.set_current_layer(self.last_layer())
        return None

def add_layer ( self , label , change_layer = True ) : self . layer_stack . insert ( self . last_layer ( ) + 1 , label ) if change_layer : self . set_current_layer ( self . last_layer ( ) ) return None


def del_layer(self, layer_num):
        """ Delete mesh layer """
        del self.layer_stack[layer_num]
        # Adjust current layer if needed
        if layer_num < self.current_layer():
            self.set_current_layer(self.current_layer() - 1)
        return None

def del_layer ( self , layer_num ) : del self . layer_stack [ layer_num ] if layer_num < self . current_layer ( ) : self . set_current_layer ( self . current_layer ( ) - 1 ) return None


def save_to_file(self, script_file):
        """ Save filter script to an mlx file """
        # TODO: rasie exception here instead?
        if not self.filters:
            print('WARNING: no filters to save to file!')
        script_file_descriptor = open(script_file, 'w')
        script_file_descriptor.write(''.join(self.opening + self.filters + self.closing))
        script_file_descriptor.close()

def
save_to_file
(
self
,
script_file
)
:
if
not
self
.
filters
:
print
(
'WARNING: no filters to save to file!'
)
script_file_descriptor
=
open
(
script_file
,
'w'
)
script_file_descriptor
.
write
(
''
.
join
(
self
.
opening
+
self
.
filters
+
self
.
closing
)
)
script_file_descriptor
.
close
(
)


def run_script(self, log=None, ml_log=None, mlp_out=None, overwrite=False,
                   file_out=None, output_mask=None, script_file=None, print_meshlabserver_output=True):
        """ Run the script
        """

        temp_script = False
        temp_ml_log = False

        if self.__no_file_in:
            # If no input files are provided, create a dummy file
            # with a single vertex and delete it first in the script.
            # This works around the fact that meshlabserver will
            # not run without an input file.
            temp_file_in_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xyz', dir=os.getcwd())
            temp_file_in_file.write(b'0 0 0')
            temp_file_in_file.close()
            self.file_in = [temp_file_in_file.name]

        if not self.filters:
            script_file = None
        elif script_file is None:
            # Create temporary script file
            temp_script = True
            temp_script_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mlx')
            temp_script_file.close()
            self.save_to_file(temp_script_file.name)
            script_file = temp_script_file.name

        if (self.parse_geometry or self.parse_topology or self.parse_hausdorff) and (ml_log is None):
            # create temp ml_log
            temp_ml_log = True
            ml_log_file = tempfile.NamedTemporaryFile(delete=False, suffix='.txt')
            ml_log_file.close()
            ml_log = ml_log_file.name
        if file_out is None:
            file_out = self.file_out

        run(script=script_file, log=log, ml_log=ml_log,
            mlp_in=self.mlp_in, mlp_out=mlp_out, overwrite=overwrite,
            file_in=self.file_in, file_out=file_out, output_mask=output_mask, ml_version=self.ml_version,
            print_meshlabserver_output=print_meshlabserver_output)

        # Parse output
        # TODO: record which layer this is associated with?
        if self.parse_geometry:
            self.geometry = compute.parse_geometry(ml_log, log, print_output=print_meshlabserver_output)
        if self.parse_topology:
            self.topology = compute.parse_topology(ml_log, log, print_output=print_meshlabserver_output)
        if self.parse_hausdorff:
            self.hausdorff_distance = compute.parse_hausdorff(ml_log, log, print_output=print_meshlabserver_output)

        # Delete temp files
        if self.__no_file_in:
            os.remove(temp_file_in_file.name)
        if temp_script:
            os.remove(temp_script_file.name)
        if temp_ml_log:
            os.remove(ml_log_file.name)

def
run_script
(
self
,
log
=
,
ml_log
=
,
mlp_out
=
,
overwrite
=
False
,
file_out
=
,
output_mask
=
,
script_file
=
,
print_meshlabserver_output
=
True
)
:
temp_script
=
False
temp_ml_log
=
False
if
self
.
__no_file_in
:
temp_file_in_file
=
tempfile
.
NamedTemporaryFile
(
delete
=
False
,
suffix
=
'.xyz'
,
dir
=
os
.
getcwd
(
)
)
temp_file_in_file
.
write
(
b'0 0 0'
)
temp_file_in_file
.
close
(
)
self
.
file_in
=
[
temp_file_in_file
.
name
]
if
not
self
.
filters
:
script_file
=
elif
script_file
is
:
temp_script
=
True
temp_script_file
=
tempfile
.
NamedTemporaryFile
(
delete
=
False
,
suffix
=
'.mlx'
)
temp_script_file
.
close
(
)
self
.
save_to_file
(
temp_script_file
.
name
)
script_file
=
temp_script_file
.
name
if
(
self
.
parse_geometry
or
self
.
parse_topology
or
self
.
parse_hausdorff
)
and
(
ml_log
is
)
:
temp_ml_log
=
True
ml_log_file
=
tempfile
.
NamedTemporaryFile
(
delete
=
False
,
suffix
=
'.txt'
)
ml_log_file
.
close
(
)
ml_log
=
ml_log_file
.
name
if
file_out
is
:
file_out
=
self
.
file_out
run
(
script
=
script_file
,
log
=
log
,
ml_log
=
ml_log
,
mlp_in
=
self
.
mlp_in
,
mlp_out
=
mlp_out
,
overwrite
=
overwrite
,
file_in
=
self
.
file_in
,
file_out
=
file_out
,
output_mask
=
output_mask
,
ml_version
=
self
.
ml_version
,
print_meshlabserver_output
=
print_meshlabserver_output
)
if
self
.
parse_geometry
:
self
.
geometry
=
compute
.
parse_geometry
(
ml_log
,
log
,
print_output
=
print_meshlabserver_output
)
if
self
.
parse_topology
:
self
.
topology
=
compute
.
parse_topology
(
ml_log
,
log
,
print_output
=
print_meshlabserver_output
)
if
self
.
parse_hausdorff
:
self
.
hausdorff_distance
=
compute
.
parse_hausdorff
(
ml_log
,
log
,
print_output
=
print_meshlabserver_output
)
if
self
.
__no_file_in
:
os
.
remove
(
temp_file_in_file
.
name
)
if
temp_script
:
os
.
remove
(
temp_script_file
.
name
)
if
temp_ml_log
:
os
.
remove
(
ml_log_file
.
name
)


def main():
    """Run main script"""
    # segments = number of segments to use for circles
    segments = 50
    # star_points = number of points (or sides) of the star
    star_points = 5
    # star_radius = radius of circle circumscribing the star
    star_radius = 2
    # ring_thickness = thickness of the colored rings
    ring_thickness = 1
    # sphere_radius = radius of sphere the shield will be deformed to
    sphere_radius = 2 * (star_radius + 3 * ring_thickness)

    # Star calculations:
    # Visually approximate a star by using multiple diamonds (i.e. scaled
    # squares) which overlap in the center. For the star calculations,
    # consider a central polygon with triangles attached to the edges, all
    # circumscribed by a circle.
    # polygon_radius = distance from center of circle to polygon edge midpoint
    polygon_radius = star_radius / \
        (1 + math.tan(math.radians(180 / star_points)) /
         math.tan(math.radians(90 / star_points)))
    # width = 1/2 width of polygon edge/outer triangle bottom
    width = polygon_radius * math.tan(math.radians(180 / star_points))
    # height = height of outer triangle
    height = width / math.tan(math.radians(90 / star_points))

    shield = mlx.FilterScript(file_out="shield.ply")

    # Create the colored front of the shield using several concentric
    # annuluses; combine them together and subdivide so we have more vertices
    # to give a smoother deformation later.
    mlx.create.annulus(shield, radius=star_radius, cir_segments=segments, color='blue')
    mlx.create.annulus(shield,
        radius1=star_radius + ring_thickness,
        radius2=star_radius,
        cir_segments=segments,
        color='red')
    mlx.create.annulus(shield,
        radius1=star_radius + 2 * ring_thickness,
        radius2=star_radius + ring_thickness,
        cir_segments=segments,
        color='white')
    mlx.create.annulus(shield,
        radius1=star_radius + 3 * ring_thickness,
        radius2=star_radius + 2 * ring_thickness,
        cir_segments=segments,
        color='red')
    mlx.layers.join(shield)
    mlx.subdivide.midpoint(shield, iterations=2)

    # Create the inside surface of the shield & translate down slightly so it
    # doesn't overlap the front.
    mlx.create.annulus(shield,
        radius1=star_radius + 3 * ring_thickness,
        cir_segments=segments,
        color='silver')
    mlx.transform.rotate(shield, axis='y', angle=180)
    mlx.transform.translate(shield, value=[0, 0, -0.005])
    mlx.subdivide.midpoint(shield, iterations=4)

    # Create a diamond for the center star. First create a plane, specifying
    # extra vertices to support the final deformation. The length from the
    # center of the plane to the corners should be 1 for ease of scaling, so
    # we use a side length of sqrt(2) (thanks Pythagoras!). Rotate the plane
    # by 45 degrees and scale it to stretch it out per the calculations above,
    # then translate it into place (including moving it up in z slightly so
    # that it doesn't overlap the shield front).
    mlx.create.grid(shield,
        size=math.sqrt(2),
        x_segments=10,
        y_segments=10,
        center=True,
        color='white')
    mlx.transform.rotate(shield, axis='z', angle=45)
    mlx.transform.scale(shield, value=[width, height, 1])
    mlx.transform.translate(shield, value=[0, polygon_radius, 0.001])

    # Duplicate the diamond and rotate the duplicates around, generating the
    # star.
    for _ in range(1, star_points):
        mlx.layers.duplicate(shield)
        mlx.transform.rotate(shield, axis='z', angle=360 / star_points)

    # Combine everything together and deform using a spherical function.
    mlx.layers.join(shield)
    mlx.transform.vert_function(shield,
        z_func='sqrt(%s-x^2-y^2)-%s+z' %
        (sphere_radius**2, sphere_radius))

    # Run the script using meshlabserver and generate the model
    shield.run_script()
    return None

def main ( ) : segments = 50 star_points = 5 star_radius = 2 ring_thickness = 1 sphere_radius = 2 * ( star_radius + 3 * ring_thickness ) polygon_radius = star_radius / ( 1 + math . tan ( math . radians ( 180 / star_points ) ) / math . tan ( math . radians ( 90 / star_points ) ) ) width = polygon_radius * math . tan ( math . radians ( 180 / star_points ) ) height = width / math . tan ( math . radians ( 90 / star_points ) ) shield = mlx . FilterScript ( file_out = "shield.ply" ) mlx . create . annulus ( shield , radius = star_radius , cir_segments = segments , color = 'blue' ) mlx . create . annulus ( shield , radius1 = star_radius + ring_thickness , radius2 = star_radius , cir_segments = segments , color = 'red' ) mlx . create . annulus ( shield , radius1 = star_radius + 2 * ring_thickness , radius2 = star_radius + ring_thickness , cir_segments = segments , color = 'white' ) mlx . create . annulus ( shield , radius1 = star_radius + 3 * ring_thickness , radius2 = star_radius + 2 * ring_thickness , cir_segments = segments , color = 'red' ) mlx . layers . join ( shield ) mlx . subdivide . midpoint ( shield , iterations = 2 ) mlx . create . annulus ( shield , radius1 = star_radius + 3 * ring_thickness , cir_segments = segments , color = 'silver' ) mlx . transform . rotate ( shield , axis = 'y' , angle = 180 ) mlx . transform . translate ( shield , value = [ 0 , 0 , - 0.005 ] ) mlx . subdivide . midpoint ( shield , iterations = 4 ) mlx . create . grid ( shield , size = math . sqrt ( 2 ) , x_segments = 10 , y_segments = 10 , center = True , color = 'white' ) mlx . transform . rotate ( shield , axis = 'z' , angle = 45 ) mlx . transform . scale ( shield , value = [ width , height , 1 ] ) mlx . transform . translate ( shield , value = [ 0 , polygon_radius , 0.001 ] ) for _ in range ( 1 , star_points ) : mlx . layers . duplicate ( shield ) mlx . transform . rotate ( shield , axis = 'z' , angle = 360 / star_points ) mlx . layers . join ( shield ) mlx . transform . vert_function ( shield , z_func = 'sqrt(%s-x^2-y^2)-%s+z' % ( sphere_radius ** 2 , sphere_radius ) ) shield . run_script ( ) return None


def hausdorff_distance(script, sampled_layer=1, target_layer=0,
                       save_sample=False, sample_vert=True, sample_edge=True,
                       sample_faux_edge=False, sample_face=True,
                       sample_num=1000, maxdist=10):
    """ Compute the Hausdorff Distance between two meshes, sampling one of the
        two and finding for each sample the closest point over the other mesh.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        sampled_layer (int): The mesh layer whose surface is sampled. For each
            sample we search the closest point on the target mesh layer.
        target_layer (int): The mesh that is sampled for the comparison.
        save_sample (bool): Save the position and distance of all the used
            samples on both the two surfaces, creating two new layers with two
            point clouds representing the used samples.
        sample_vert (bool): For the search of maxima it is useful to sample
            vertices and edges of the mesh with a greater care. It is quite
            probable that the farthest points falls along edges or on mesh
            vertexes, and with uniform montecarlo sampling approaches the
            probability of taking a sample over a vertex or an edge is
            theoretically null. On the other hand this kind of sampling could
            make the overall sampling distribution slightly biased and slightly
            affects the cumulative results.
        sample_edge (bool): see sample_vert
        sample_faux_edge (bool): see sample_vert
        sample_face (bool): see sample_vert
        sample_num (int): The desired number of samples. It can be smaller or
            larger than the mesh size, and according to the chosen sampling
            strategy it will try to adapt.
        maxdist (int): Sample points for which we do not find anything within
            this distance are rejected and not considered neither for averaging
            nor for max.

    Layer stack:
        If save_sample is True, two new layers are created: 'Hausdorff Closest
            Points' and 'Hausdorff Sample Point'; and the current layer is
            changed to the last newly created layer.
        If save_sample is False, no impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    # MeshLab defaults:
    #  sample_num = number of vertices
    #  maxdist = 0.05 * AABB['diag'] #5% of AABB[diag]
    #  maxdist_max = AABB['diag']
    maxdist_max = 2*maxdist
    # TODO: parse output (min, max, mean, etc.)
    filter_xml = ''.join([
        '  <filter name="Hausdorff Distance">\n',
        '    <Param name="SampledMesh" ',
        'value="{:d}" '.format(sampled_layer),
        'description="Sampled Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="TargetMesh" ',
        'value="{:d}" '.format(target_layer),
        'description="Target Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="SaveSample" ',
        'value="{}" '.format(str(save_sample).lower()),
        'description="Save Samples" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleVert" ',
        'value="{}" '.format(str(sample_vert).lower()),
        'description="Sample Vertexes" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleEdge" ',
        'value="{}" '.format(str(sample_edge).lower()),
        'description="Sample Edges" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleFauxEdge" ',
        'value="{}" '.format(str(sample_faux_edge).lower()),
        'description="Sample FauxEdge" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleFace" ',
        'value="{}" '.format(str(sample_face).lower()),
        'value="%s" ' % str(sample_face).lower() +
        'description="Sample Faces" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleNum" ',
        'value="{:d}" '.format(sample_num),
        'description="Number of samples" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="MaxDist" ',
        'value="{}" '.format(maxdist),
        'value="%s" ' % maxdist +
        'description="Max Distance" ',
        'min="0" ',
        'max="{}" '.format(maxdist_max),
        'type="RichAbsPerc" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.parse_hausdorff = True
    if isinstance(script, FilterScript) and save_sample:
        script.add_layer('Hausdorff Closest Points')
        script.add_layer('Hausdorff Sample Point')
    return None

'  <filter name="Hausdorff Distance">\n'

Compute the Hausdorff Distance between two meshes sampling one of the two and finding for each sample the closest point over the other mesh .


def poisson_disk(script, sample_num=1000, radius=0.0,
                 montecarlo_rate=20, save_montecarlo=False,
                 approx_geodesic_dist=False, subsample=False, refine=False,
                 refine_layer=0, best_sample=True, best_sample_pool=10,
                 exact_num=False, radius_variance=1.0):
    """ Create a new layer populated with a point sampling of the current mesh.

    Samples are generated according to a Poisson-disk distribution, using the
    algorithm described in:

    'Efficient and Flexible Sampling with Blue Noise Properties of Triangular Meshes'
    Massimiliano Corsini, Paolo Cignoni, Roberto Scopigno
    IEEE TVCG 2012

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        sample_num (int): The desired number of samples. The radius of the disk
            is calculated according to the sampling density.
        radius (float): If not zero this parameter overrides the previous
            parameter to allow exact radius specification.
        montecarlo_rate (int): The over-sampling rate that is used to generate
            the intial Monte Carlo samples (e.g. if this parameter is 'K' means
            that 'K * sample_num' points will be used). The generated
            Poisson-disk samples are a subset of these initial Monte Carlo
            samples. Larger numbers slow the process but make it a bit more
            accurate.
        save_montecarlo (bool): If True, it will generate an additional Layer
            with the Monte Carlo sampling that was pruned to build the Poisson
            distribution.
        approx_geodesic_dist (bool): If True Poisson-disk distances are
            computed using an approximate geodesic distance, e.g. an Euclidean
            distance weighted by a function of the difference between the
            normals of the two points.
        subsample (bool): If True the original vertices of the base mesh are
            used as base set of points. In this case the sample_num should be
            obviously much smaller than the original vertex number. Note that
            this option is very useful in the case you want to subsample a
            dense point cloud.
        refine (bool): If True the vertices of the refine_layer mesh layer are
            used as starting vertices, and they will be utterly refined by
            adding more and more points until possible.
        refine_layer (int): Used only if refine is True.
        best_sample (bool): If True it will use a simple heuristic for choosing
            the samples. At a small cost (it can slow the process a bit) it
            usually improves the maximality of the generated sampling.
        best_sample_pool (bool): Used only if best_sample is True. It controls
            the number of attempts that it makes to get the best sample. It is
            reasonable that it is smaller than the Monte Carlo oversampling
            factor.
        exact_num (bool): If True it will try to do a dicotomic search for the
            best Poisson-disk radius that will generate the requested number of
            samples with a tolerance of the 0.5%. Obviously it takes much
            longer.
        radius_variance (float): The radius of the disk is allowed to vary
            between r and r*var. If this parameter is 1 the sampling is the
            same as the Poisson-disk Sampling.

    Layer stack:
        Creates new layer 'Poisson-disk Samples'. Current layer is NOT changed
            to the new layer (see Bugs).
        If save_montecarlo is True, creates a new layer 'Montecarlo Samples'.
            Current layer is NOT changed to the new layer (see Bugs).

    MeshLab versions:
        2016.12
        1.3.4BETA

    Bugs:
        Current layer is NOT changed to the new layer, which is inconsistent
            with the majority of filters that create new layers.
    """
    filter_xml = ''.join([
        '  <filter name="Poisson-disk Sampling">\n',
        '    <Param name="SampleNum" ',
        'value="{:d}" '.format(sample_num),
        'description="Number of samples" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Radius" ',
        'value="{}" '.format(radius),
        'description="Explicit Radius" ',
        'min="0" ',
        'max="100" ',
        'type="RichAbsPerc" ',
        '/>\n',
        '    <Param name="MontecarloRate" ',
        'value="{:d}" '.format(montecarlo_rate),
        'description="MonterCarlo OverSampling" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="SaveMontecarlo" ',
        'value="{}" '.format(str(save_montecarlo).lower()),
        'description="Save Montecarlo" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="ApproximateGeodesicDistance" ',
        'value="{}" '.format(str(approx_geodesic_dist).lower()),
        'description="Approximate Geodesic Distance" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="Subsample" ',
        'value="{}" '.format(str(subsample).lower()),
        'description="Base Mesh Subsampling" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="RefineFlag" ',
        'value="{}" '.format(str(refine).lower()),
        'description="Refine Existing Samples" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="RefineMesh" ',
        'value="{:d}" '.format(refine_layer),
        'description="Samples to be refined" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="BestSampleFlag" ',
        'value="{}" '.format(str(best_sample).lower()),
        'description="Best Sample Heuristic" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="BestSamplePool" ',
        'value="{:d}" '.format(best_sample_pool),
        'description="Best Sample Pool Size" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="ExactNumFlag" ',
        'value="{}" '.format(str(exact_num).lower()),
        'description="Exact number of samples" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="RadiusVariance" ',
        'value="{}" '.format(radius_variance),
        'description="Radius Variance" ',
        'type="RichFloat" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Poisson-disk Samples')
        if save_montecarlo:
            script.add_layer('Montecarlo Samples')
    return None

Create a new layer populated with a point sampling of the current mesh .


def mesh_element(script, sample_num=1000, element='VERT'):
    """ Create a new layer populated with a point sampling of the current mesh,
        at most one sample for each element of the mesh is created.

    Samples are taking in a uniform way, one for each element
    (vertex/edge/face); all the elements have the same probabilty of being
    choosen.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        sample_num (int): The desired number of elements that must be chosen.
            Being a subsampling of the original elements if this number should
            not be larger than the number of elements of the original mesh.
        element (enum in ['VERT', 'EDGE', 'FACE']): Choose what mesh element
            will be used for the subsampling. At most one point sample will
            be added for each one of the chosen elements

    Layer stack:
        Creates new layer 'Sampled Mesh'. Current layer is changed to the new
            layer.

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if element.lower() == 'vert':
        element_num = 0
    elif element.lower() == 'edge':
        element_num = 1
    elif element.lower() == 'face':
        element_num = 2
    filter_xml = ''.join([
        '  <filter name="Mesh Element Subsampling">\n',
        '    <Param name="Sampling" ',
        'value="{:d}" '.format(element_num),
        'description="Element to sample:" ',
        'enum_val0="Vertex" ',
        'enum_val1="Edge" ',
        'enum_val2="Face" ',
        'enum_cardinality="3" ',
        'type="RichEnum" ',
        '/>\n',
        '    <Param name="SampleNum" ',
        'value="{:d}" '.format(sample_num),
        'description="Number of samples" ',
        'type="RichInt" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Sampled Mesh')
    return None

Create a new layer populated with a point sampling of the current mesh at most one sample for each element of the mesh is created .


def clustered_vert(script, cell_size=1.0, strategy='AVERAGE', selected=False):
    """ "Create a new layer populated with a subsampling of the vertexes of the
        current mesh

    The subsampling is driven by a simple one-per-gridded cell strategy.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        cell_size (float): The size of the cell of the clustering grid. Smaller the cell finer the resulting mesh. For obtaining a very coarse mesh use larger values.
        strategy (enum 'AVERAGE' or 'CENTER'): Average: for each cell we take the average of the sample falling into. The resulting point is a new point.
Closest to center: for each cell we take the sample that is closest to the center of the cell. Choosen vertices are a subset of the original ones.
        selected (bool): If true only for the filter is applied only on the selected subset of the mesh.

    Layer stack:
        Creates new layer 'Cluster Samples'. Current layer is changed to the new
            layer.

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if strategy.lower() == 'average':
        strategy_num = 0
    elif strategy.lower() == 'center':
        strategy_num = 1

    filter_xml = ''.join([
        '  <filter name="Clustered Vertex Subsampling">\n',
        '    <Param name="Threshold" ',
        'value="{}" '.format(cell_size),
        'description="Cell Size" ',
        'min="0" ',
        'max="1000" ',
        'type="RichAbsPerc" ',
        '/>\n',
        '    <Param name="Sampling" ',
        'value="{:d}" '.format(strategy_num),
        'description="Representative Strategy:" ',
        'enum_val0="Average" ',
        'enum_val1="Closest to center" ',
        'enum_cardinality="2" ',
        'type="RichEnum" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Selected" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Cluster Samples')
    return None


def flat_plane(script, plane=0, aspect_ratio=False):
    """Flat plane parameterization

    """
    filter_xml = ''.join([
        '  <filter name="Parametrization: Flat Plane ">\n',
        '    <Param name="projectionPlane"',
        'value="%d"' % plane,
        'description="Projection plane"',
        'enum_val0="XY"',
        'enum_val1="XZ"',
        'enum_val2="YZ"',
        'enum_cardinality="3"',
        'type="RichEnum"',
        'tooltip="Choose the projection plane"',
        '/>\n',
        '    <Param name="aspectRatio"',
        'value="%s"' % str(aspect_ratio).lower(),
        'description="Preserve Ratio"',
        'type="RichBool"',
        'tooltip="If checked the resulting parametrization will preserve the original apsect ratio of the model otherwise it will fill up the whole 0..1 uv space"',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def per_triangle(script, sidedim=0, textdim=1024, border=2, method=1):
    """Trivial Per-Triangle parameterization

    """
    filter_xml = ''.join([
        '  <filter name="Parametrization: Trivial Per-Triangle ">\n',
        '    <Param name="sidedim"',
        'value="%d"' % sidedim,
        'description="Quads per line"',
        'type="RichInt"',
        'tooltip="Indicates how many triangles have to be put on each line (every quad contains two triangles). Leave 0 for automatic calculation"',
        '/>\n',
        '    <Param name="textdim"',
        'value="%d"' % textdim,
        'description="Texture Dimension (px)"',
        'type="RichInt"',
        'tooltip="Gives an indication on how big the texture is"',
        '/>\n',
        '    <Param name="border"',
        'value="%d"' % border,
        'description="Inter-Triangle border (px)"',
        'type="RichInt"',
        'tooltip="Specifies how many pixels to be left between triangles in parametrization domain"',
        '/>\n',
        '    <Param name="method"',
        'value="%d"' % method,
        'description="Method"',
        'enum_val0="Basic"',
        'enum_val1="Space-optimizing"',
        'enum_cardinality="2"',
        'type="RichEnum"',
        'tooltip="Choose space optimizing to map smaller faces into smaller triangles in parametrizazion domain"'
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def voronoi(script, region_num=10, overlap=False):
    """Voronoi Atlas parameterization

    """
    filter_xml = ''.join([
        '  <filter name="Parametrization: Voronoi Atlas">\n',
        '    <Param name="regionNum"',
        'value="%d"' % region_num,
        'description="Approx. Region Num"',
        'type="RichInt"',
        'tooltip="An estimation of the number of regions that must be generated. Smaller regions could lead to parametrizations with smaller distortion."',
        '/>\n',
        '    <Param name="overlapFlag"',
        'value="%s"' % str(overlap).lower(),
        'description="Overlap"',
        'type="RichBool"',
        'tooltip="If checked the resulting parametrization will be composed by overlapping regions, e.g. the resulting mesh will have duplicated faces: each region will have a ring of ovelapping duplicate faces that will ensure that border regions will be parametrized in the atlas twice. This is quite useful for building mipmap robust atlases"',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def measure_topology(script):
    """ Compute a set of topological measures over a mesh

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = '  <xmlfilter name="Compute Topological Measures"/>\n'
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.parse_topology = True
    return None


def parse_topology(ml_log, log=None, ml_version='1.3.4BETA', print_output=False):
    """Parse the ml_log file generated by the measure_topology function.

    Args:
        ml_log (str): MeshLab log file to parse
        log (str): filename to log output

    Returns:
        dict: dictionary with the following keys:
            vert_num (int): number of vertices
            edge_num (int): number of edges
            face_num (int): number of faces
            unref_vert_num (int): number or unreferenced vertices
            boundry_edge_num (int): number of boundary edges
            part_num (int): number of parts (components) in the mesh.
            manifold (bool): True if mesh is two-manifold, otherwise false.
            non_manifold_edge (int): number of non_manifold edges.
            non_manifold_vert (int): number of non-manifold verices
            genus (int or str): genus of the mesh, either a number or
                'undefined' if the mesh is non-manifold.
            holes (int or str): number of holes in the mesh, either a number
                or 'undefined' if the mesh is non-manifold.

    """
    topology = {'manifold': True, 'non_manifold_E': 0, 'non_manifold_V': 0}
    with open(ml_log) as fread:
        for line in fread:
            if 'V:' in line:
                vert_edge_face = line.replace('V:', ' ').replace('E:', ' ').replace('F:', ' ').split()
                topology['vert_num'] = int(vert_edge_face[0])
                topology['edge_num'] = int(vert_edge_face[1])
                topology['face_num'] = int(vert_edge_face[2])
            if 'Unreferenced Vertices' in line:
                topology['unref_vert_num'] = int(line.split()[2])
            if 'Boundary Edges' in line:
                topology['boundry_edge_num'] = int(line.split()[2])
            if 'Mesh is composed by' in line:
                topology['part_num'] = int(line.split()[4])
            if 'non 2-manifold mesh' in line:
                topology['manifold'] = False
            if 'non two manifold edges' in line:
                topology['non_manifold_edge'] = int(line.split()[2])
            if 'non two manifold vertexes' in line:
                topology['non_manifold_vert'] = int(line.split()[2])
            if 'Genus is' in line:  # undefined or int
                topology['genus'] = line.split()[2]
                if topology['genus'] != 'undefined':
                    topology['genus'] = int(topology['genus'])
            if 'holes' in line:
                topology['hole_num'] = line.split()[2]
                if topology['hole_num'] == 'a':
                    topology['hole_num'] = 'undefined'
                else:
                    topology['hole_num'] = int(topology['hole_num'])
    for key, value in topology.items():
        if log is not None:
            log_file = open(log, 'a')
            log_file.write('{:16} = {}\n'.format(key, value))
            log_file.close()
        elif print_output:
            print('{:16} = {}'.format(key, value))

    return topology

def
parse_topology
(
ml_log
,
log
=
,
ml_version
=
'1.3.4BETA'
,
print_output
=
False
)
:
topology
=
{
'manifold'
:
True
,
'non_manifold_E'
:
0
,
'non_manifold_V'
:
0
}
with
open
(
ml_log
)
as
fread
:
for
line
in
fread
:
if
'V:'
in
line
:
vert_edge_face
=
line
.
replace
(
'V:'
,
' '
)
.
replace
(
'E:'
,
' '
)
.
replace
(
'F:'
,
' '
)
.
split
(
)
topology
[
'vert_num'
]
=
int
(
vert_edge_face
[
0
]
)
topology
[
'edge_num'
]
=
int
(
vert_edge_face
[
1
]
)
topology
[
'face_num'
]
=
int
(
vert_edge_face
[
2
]
)
if
'Unreferenced Vertices'
in
line
:
topology
[
'unref_vert_num'
]
=
int
(
line
.
split
(
)
[
2
]
)
if
'Boundary Edges'
in
line
:
topology
[
'boundry_edge_num'
]
=
int
(
line
.
split
(
)
[
2
]
)
if
'Mesh is composed by'
in
line
:
topology
[
'part_num'
]
=
int
(
line
.
split
(
)
[
4
]
)
if
'non 2-manifold mesh'
in
line
:
topology
[
'manifold'
]
=
False
if
'non two manifold edges'
in
line
:
topology
[
'non_manifold_edge'
]
=
int
(
line
.
split
(
)
[
2
]
)
if
'non two manifold vertexes'
in
line
:
topology
[
'non_manifold_vert'
]
=
int
(
line
.
split
(
)
[
2
]
)
if
'Genus is'
in
line
:
topology
[
'genus'
]
=
line
.
split
(
)
[
2
]
if
topology
[
'genus'
]
!=
'undefined'
:
topology
[
'genus'
]
=
int
(
topology
[
'genus'
]
)
if
'holes'
in
line
:
topology
[
'hole_num'
]
=
line
.
split
(
)
[
2
]
if
topology
[
'hole_num'
]
==
'a'
:
topology
[
'hole_num'
]
=
'undefined'
else
:
topology
[
'hole_num'
]
=
int
(
topology
[
'hole_num'
]
)
for
key
,
value
in
topology
.
items
(
)
:
if
log
is
not
:
log_file
=
open
(
log
,
'a'
)
log_file
.
write
(
'{:16} = {}\n'
.
format
(
key
,
value
)
)
log_file
.
close
(
)
elif
print_output
:
print
(
'{:16} = {}'
.
format
(
key
,
value
)
)
return
topology

Parse the ml_log file generated by the measure_topology function .


def parse_hausdorff(ml_log, log=None, print_output=False):
    """Parse the ml_log file generated by the hausdorff_distance function.

    Args:
        ml_log (str): MeshLab log file to parse
        log (str): filename to log output

    Returns:
        dict: dictionary with the following keys:
            number_points (int): number of points in mesh
            min_distance (float): minimum hausdorff distance
            max_distance (float): maximum hausdorff distance
            mean_distance (float): mean hausdorff distance
            rms_distance (float): root mean square distance

    """
    hausdorff_distance = {"min_distance": 0.0,
                          "max_distance": 0.0,
                          "mean_distance": 0.0,
                          "rms_distance": 0.0,
                          "number_points": 0}
    with open(ml_log) as fread:
        result = fread.readlines()
        data = ""

        for idx, line in enumerate(result):
            m = re.match(r"\s*Sampled (\d+) pts.*", line)
            if m is not None:
                hausdorff_distance["number_points"] = int(m.group(1))
            if 'Hausdorff Distance computed' in line:
                data = result[idx + 2]

        m = re.match(r"\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)", data)
        hausdorff_distance["min_distance"] = float(m.group(1))
        hausdorff_distance["max_distance"] = float(m.group(2))
        hausdorff_distance["mean_distance"] = float(m.group(3))
        hausdorff_distance["rms_distance"] = float(m.group(4))
        for key, value in hausdorff_distance.items():
            if log is not None:
                log_file = open(log, 'a')
                log_file.write('{:16} = {}\n'.format(key, value))
                log_file.close()
            elif print_output:
                print('{:16} = {}'.format(key, value))
        return hausdorff_distance

Parse the ml_log file generated by the hausdorff_distance function .


def function(script, red=255, green=255, blue=255, alpha=255, color=None):
    """Color function using muparser lib to generate new RGBA color for every
        vertex

    Red, Green, Blue and Alpha channels may be defined by specifying a function
    for each.

    See help(mlx.muparser_ref) for muparser reference documentation.

    It's possible to use the following per-vertex variables in the expression:

    Variables (per vertex):
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad (radius)
        vi (vertex index)
        vtu, vtv (texture coordinates)
        ti (texture index)
        vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        red (str [0, 255]): function to generate red component
        green (str [0, 255]): function to generate green component
        blue (str [0, 255]): function to generate blue component
        alpha (str [0, 255]): function to generate alpha component
        color (str): name of one of the 140 HTML Color Names defined
            in CSS & SVG.
            Ref: https://en.wikipedia.org/wiki/Web_colors#X11_color_names
            If not None this will override the per component variables.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    # TODO: add options for HSV
    # https://www.cs.rit.edu/~ncs/color/t_convert.html
    if color is not None:
        red, green, blue, _ = color_name[color.lower()]
    filter_xml = ''.join([
        '  <filter name="Per Vertex Color Function">\n',
        '    <Param name="x" ',
        'value="{}" '.format(str(red).replace('&', '&').replace('<', '<')),
        'description="func r = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="y" ',
        'value="{}" '.format(str(green).replace('&', '&').replace('<', '<')),
        'description="func g = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="z" ',
        'value="{}" '.format(str(blue).replace('&', '&').replace('<', '<')),
        'description="func b = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="a" ',
        'value="{}" '.format(str(alpha).replace('&', '&').replace('<', '<')),
        'description="func alpha = " ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def voronoi(script, target_layer=0, source_layer=1, backward=True):
    """ Given a Mesh 'M' and a Pointset 'P', the filter projects each vertex of
        P over M and color M according to the geodesic distance from these
        projected points. Projection and coloring are done on a per vertex
        basis.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        target_layer (int): The mesh layer whose surface is colored. For each
            vertex of this mesh we decide the color according to the following
            arguments.
        source_layer (int): The mesh layer whose vertexes are used as seed
            points for the color computation. These seeds point are projected
            onto the target_layer mesh.
        backward (bool): If True the mesh is colored according to the distance
            from the frontier of the voronoi diagram induced by the
            source_layer seeds.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Voronoi Vertex Coloring">\n',
        '    <Param name="ColoredMesh" ',
        'value="{:d}" '.format(target_layer),
        'description="To be Colored Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="VertexMesh" ',
        'value="{:d}" '.format(source_layer),
        'description="Vertex Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="backward" ',
        'value="{}" '.format(str(backward).lower()),
        'description="BackDistance" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Given a Mesh M and a Pointset P the filter projects each vertex of P over M and color M according to the geodesic distance from these projected points . Projection and coloring are done on a per vertex basis .


def cyclic_rainbow(script, direction='sphere', start_pt=(0, 0, 0),
                   amplitude=255 / 2, center=255 / 2, freq=0.8,
                   phase=(0, 120, 240, 0), alpha=False):
    """ Color mesh vertices in a repeating sinusiodal rainbow pattern

    Sine wave follows the following equation for each color channel (RGBA):
    channel = sin(freq*increment + phase)*amplitude + center

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        direction (str) = the direction that the sine wave will travel; this
            and the start_pt determine the 'increment' of the sine function.
            Valid values are:
            'sphere' - radiate sine wave outward from start_pt (default)
            'x' - sine wave travels along the X axis
            'y' - sine wave travels along the Y axis
            'z' - sine wave travels along the Z axis
            or define the increment directly using a muparser function, e.g.
                '2x + y'. In this case start_pt will not be used; include it in
                the function directly.
    start_pt (3 coordinate tuple or list): start point of the sine wave. For a
        sphere this is the center of the sphere.
    amplitude (float [0, 255], single value or 4 term tuple or list): amplitude
        of the sine wave, with range between 0-255. If a single value is
        specified it will be used for all channels, otherwise specify each
        channel individually.
    center (float [0, 255], single value or 4 term tuple or list): center
        of the sine wave, with range between 0-255. If a single value is
        specified it will be used for all channels, otherwise specify each
        channel individually.
    freq (float, single value or 4 term tuple or list): frequency of the sine
        wave. If a single value is specified it will be used for all channels,
        otherwise specifiy each channel individually.
    phase (float [0, 360], single value or 4 term tuple or list): phase
        of the sine wave in degrees, with range between 0-360. If a single
        value is specified it will be used for all channels, otherwise specify
        each channel individually.
    alpha (bool): if False the alpha channel will be set to 255 (full opacity).

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    start_pt = util.make_list(start_pt, 3)
    amplitude = util.make_list(amplitude, 4)
    center = util.make_list(center, 4)
    freq = util.make_list(freq, 4)
    phase = util.make_list(phase, 4)

    if direction.lower() == 'sphere':
        increment = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)'.format(
            start_pt[0], start_pt[1], start_pt[2])
    elif direction.lower() == 'x':
        increment = 'x - {}'.format(start_pt[0])
    elif direction.lower() == 'y':
        increment = 'y - {}'.format(start_pt[1])
    elif direction.lower() == 'z':
        increment = 'z - {}'.format(start_pt[2])
    else:
        increment = direction

    red_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
        f=freq[0], i=increment, p=math.radians(phase[0]),
        a=amplitude[0], c=center[0])
    green_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
        f=freq[1], i=increment, p=math.radians(phase[1]),
        a=amplitude[1], c=center[1])
    blue_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
        f=freq[2], i=increment, p=math.radians(phase[2]),
        a=amplitude[2], c=center[2])
    if alpha:
        alpha_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
            f=freq[3], i=increment, p=math.radians(phase[3]),
            a=amplitude[3], c=center[3])
    else:
        alpha_func = 255

    function(script, red=red_func, green=green_func, blue=blue_func,
             alpha=alpha_func)
    return None

def
cyclic_rainbow
(
script
,
direction
=
'sphere'
,
start_pt
=
(
0
,
0
,
0
)
,
amplitude
=
255
/
2
,
center
=
255
/
2
,
freq
=
0.8
,
phase
=
(
0
,
120
,
240
,
0
)
,
alpha
=
False
)
:
start_pt
=
util
.
make_list
(
start_pt
,
3
)
amplitude
=
util
.
make_list
(
amplitude
,
4
)
center
=
util
.
make_list
(
center
,
4
)
freq
=
util
.
make_list
(
freq
,
4
)
phase
=
util
.
make_list
(
phase
,
4
)
if
direction
.
lower
(
)
==
'sphere'
:
increment
=
'sqrt((x-{})^2+(y-{})^2+(z-{})^2)'
.
format
(
start_pt
[
0
]
,
start_pt
[
1
]
,
start_pt
[
2
]
)
elif
direction
.
lower
(
)
==
'x'
:
increment
=
'x - {}'
.
format
(
start_pt
[
0
]
)
elif
direction
.
lower
(
)
==
'y'
:
increment
=
'y - {}'
.
format
(
start_pt
[
1
]
)
elif
direction
.
lower
(
)
==
'z'
:
increment
=
'z - {}'
.
format
(
start_pt
[
2
]
)
else
:
increment
=
direction
red_func
=
'{a}*sin({f}*{i} + {p}) + {c}'
.
format
(
f
=
freq
[
0
]
,
i
=
increment
,
p
=
math
.
radians
(
phase
[
0
]
)
,
a
=
amplitude
[
0
]
,
c
=
center
[
0
]
)
green_func
=
'{a}*sin({f}*{i} + {p}) + {c}'
.
format
(
f
=
freq
[
1
]
,
i
=
increment
,
p
=
math
.
radians
(
phase
[
1
]
)
,
a
=
amplitude
[
1
]
,
c
=
center
[
1
]
)
blue_func
=
'{a}*sin({f}*{i} + {p}) + {c}'
.
format
(
f
=
freq
[
2
]
,
i
=
increment
,
p
=
math
.
radians
(
phase
[
2
]
)
,
a
=
amplitude
[
2
]
,
c
=
center
[
2
]
)
if
alpha
:
alpha_func
=
'{a}*sin({f}*{i} + {p}) + {c}'
.
format
(
f
=
freq
[
3
]
,
i
=
increment
,
p
=
math
.
radians
(
phase
[
3
]
)
,
a
=
amplitude
[
3
]
,
c
=
center
[
3
]
)
else
:
alpha_func
=
255
function
(
script
,
red
=
red_func
,
green
=
green_func
,
blue
=
blue_func
,
alpha
=
alpha_func
)
return


def mp_atan2(y, x):
    """muparser atan2 function

    Implements an atan2(y,x) function for older muparser versions (<2.1.0);
    atan2 was added as a built-in function in muparser 2.1.0

    Args:
        y (str): y argument of the atan2(y,x) function
        x (str): x argument of the atan2(y,x) function

    Returns:
        A muparser string that calculates atan2(y,x)
    """
    return 'if((x)>0, atan((y)/(x)), if(((x)<0) and ((y)>=0), atan((y)/(x))+pi, if(((x)<0) and ((y)<0), atan((y)/(x))-pi, if(((x)==0) and ((y)>0), pi/2, if(((x)==0) and ((y)<0), -pi/2, 0)))))'.replace(
        'pi', str(math.pi)).replace('y', y).replace('x', x)


def v_cross(u, v):
    """muparser cross product function

    Compute the cross product of two 3x1 vectors

    Args:
        u (list or tuple of 3 strings): first vector
        v (list or tuple of 3 strings): second vector
    Returns:
        A list containing a muparser string of the cross product
    """
    """
    i = u[1]*v[2] - u[2]*v[1]
    j = u[2]*v[0] - u[0]*v[2]
    k = u[0]*v[1] - u[1]*v[0]
    """

    i = '(({u1})*({v2}) - ({u2})*({v1}))'.format(u1=u[1], u2=u[2], v1=v[1], v2=v[2])
    j = '(({u2})*({v0}) - ({u0})*({v2}))'.format(u0=u[0], u2=u[2], v0=v[0], v2=v[2])
    k = '(({u0})*({v1}) - ({u1})*({v0}))'.format(u0=u[0], u1=u[1], v0=v[0], v1=v[1])
    return [i, j, k]


def v_multiply(scalar, v1):
    """ Multiply vector by scalar"""
    vector = []
    for i, x in enumerate(v1):
        vector.append('(({})*({}))'.format(scalar, v1[i]))
    return vector

def v_multiply ( scalar , v1 ) : vector = [ ] for i , x in enumerate ( v1 ) : vector . append ( '(({})*({}))' . format ( scalar , v1 [ i ] ) ) return vector


def vert_attr(script, name='radius', function='x^2 + y^2'):
    """ Add a new Per-Vertex scalar attribute to current mesh and fill it with
        the defined function.

    The specified name can be used in other filter functions.

    It's possible to use parenthesis, per-vertex variables and boolean operator:
        (, ), and, or, <, >, =
    It's possible to use the following per-vertex variables in the expression:

    Variables:
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad
        vi (vertex index)
        ?vtu, vtv (texture coordinates)
        ?ti (texture index)
        ?vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        name (str): the name of new attribute. You can access attribute in
            other filters through this name.
        function (str): function to calculate custom attribute value for each
            vertex

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Define New Per Vertex Attribute">\n',
        '    <Param name="name" ',
        'value="{}" '.format(name),
        'description="Name" ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="expr" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="Function" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Add a new Per - Vertex scalar attribute to current mesh and fill it with the defined function .


def flip(script, force_flip=False, selected=False):
    """ Invert faces orientation, flipping the normals of the mesh.

    If requested, it tries to guess the right orientation; mainly it decides to
    flip all the faces if the minimum/maximum vertexes have not outward point
    normals for a few directions. Works well for single component watertight
    objects.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        force_flip (bool): If selected, the normals will always be flipped;
            otherwise, the filter tries to set them outside.
        selected (bool): If selected, only selected faces will be affected.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Invert Faces Orientation">\n',
        '    <Param name="forceFlip" ',
        'value="{}" '.format(str(force_flip).lower()),
        'description="Force Flip" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="onlySelected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Flip only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Invert faces orientation flipping the normals of the mesh .


def point_sets(script, neighbors=10, smooth_iteration=0, flip=False,
               viewpoint_pos=(0.0, 0.0, 0.0)):
    """ Compute the normals of the vertices of a mesh without exploiting the
        triangle connectivity, useful for dataset with no faces.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        neighbors (int): The number of neighbors used to estimate normals.
        smooth_iteration (int): The number of smoothing iteration done on the
            p used to estimate and propagate normals.
        flip (bool): Flip normals w.r.t. viewpoint. If the 'viewpoint' (i.e.
            scanner position) is known, it can be used to disambiguate normals
            orientation, so that all the normals will be oriented in the same
            direction.
        viewpoint_pos (single xyz point, tuple or list): Set the x, y, z
            coordinates of the viewpoint position.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Compute normals for point sets">\n',
        '    <Param name="K" ',
        'value="{:d}" '.format(neighbors),
        'description="Neighbour num" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="smoothIter" ',
        'value="{:d}" '.format(smooth_iteration),
        'description="Smooth Iteration" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="flipFlag" ',
        'value="{}" '.format(str(flip).lower()),
        'description="Flip normals w.r.t. viewpoint" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="viewPos" ',
        'x="{}" y="{}" z="{}" '.format(viewpoint_pos[0], viewpoint_pos[1],
                                       viewpoint_pos[2],),
        'description="Viewpoint Pos." ',
        'type="RichPoint3f" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Compute the normals of the vertices of a mesh without exploiting the triangle connectivity useful for dataset with no faces .


def taubin(script, iterations=10, t_lambda=0.5, t_mu=-0.53, selected=False):
    """ The lambda & mu Taubin smoothing, it make two steps of smoothing, forth
        and back, for each iteration.

    Based on:
    Gabriel Taubin
    "A signal processing approach to fair surface design"
    Siggraph 1995

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): The number of times that the taubin smoothing is
            iterated. Usually it requires a larger number of iteration than the
            classical laplacian.
        t_lambda (float): The lambda parameter of the Taubin Smoothing algorithm
        t_mu (float): The mu parameter of the Taubin Smoothing algorithm
        selected (bool): If selected the filter is performed only on the
            selected faces

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Taubin Smooth">\n',
        '    <Param name="lambda" ',
        'value="{}" '.format(t_lambda),
        'description="Lambda" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="mu" ',
        'value="{}" '.format(t_mu),
        'description="mu" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="stepSmoothNum" ',
        'value="{:d}" '.format(iterations),
        'description="Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

The lambda & mu Taubin smoothing it make two steps of smoothing forth and back for each iteration .


def depth(script, iterations=3, viewpoint=(0, 0, 0), selected=False):
    """ A laplacian smooth that is constrained to move vertices only along the
        view direction.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): The number of times that the whole algorithm (normal
            smoothing + vertex fitting) is iterated.
        viewpoint (vector tuple or list): The position of the view point that
            is used to get the constraint direction.
        selected (bool): If selected the filter is performed only on the
            selected faces

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Depth Smooth">\n',
        '    <Param name="stepSmoothNum" ',
        'value="{:d}" '.format(iterations),
        'description="Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="viewPoint" ',
        'x="{}" '.format(viewpoint[0]),
        'y="{}" '.format(viewpoint[1]),
        'z="{}" '.format(viewpoint[2]),
        'description="Smoothing steps" ',
        'type="RichPoint3f" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

A laplacian smooth that is constrained to move vertices only along the view direction .


def polylinesort(fbasename=None, log=None):
    """Sort separate line segments in obj format into a continuous polyline or polylines.
    NOT FINISHED; DO NOT USE

    Also measures the length of each polyline

    Return polyline and polylineMeta (lengths)

    """
    fext = os.path.splitext(fbasename)[1][1:].strip().lower()
    if fext != 'obj':
        print('Input file must be obj. Exiting ...')
        sys.exit(1)
    fread = open(fbasename, 'r')
    first = True
    polyline_vertices = []
    line_segments = []
    for line in fread:
        element, x_co, y_co, z_co = line.split()
        if element == 'v':
            polyline_vertices.append(
                [util.to_float(x_co), util.to_float(y_co), util.to_float(z_co)])
        elif element == 'l':
            p1 = x_co
            p2 = y_co
            line_segments.append([int(p1), int(p2)])

    fread.close()
    if log is not None:
        log_file = open(log, 'a')
        #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename)
        """log_file.write('min = %s\n' % aabb['min'])
        log_file.write('max = %s\n' % aabb['max'])
        log_file.write('center = %s\n' % aabb['center'])
        log_file.write('size = %s\n' % aabb['size'])
        log_file.write('diagonal = %s\n' % aabb['diagonal'])"""
        log_file.close()
    # print(aabb)
    return None

def
polylinesort
(
fbasename
=
,
log
=
)
:
fext
=
os
.
path
.
splitext
(
fbasename
)
[
1
]
[
1
:
]
.
strip
(
)
.
lower
(
)
if
fext
!=
'obj'
:
print
(
'Input file must be obj. Exiting ...'
)
sys
.
exit
(
1
)
fread
=
open
(
fbasename
,
'r'
)
first
=
True
polyline_vertices
=
[
]
line_segments
=
[
]
for
line
in
fread
:
element
,
x_co
,
y_co
,
z_co
=
line
.
split
(
)
if
element
==
'v'
:
polyline_vertices
.
append
(
[
util
.
to_float
(
x_co
)
,
util
.
to_float
(
y_co
)
,
util
.
to_float
(
z_co
)
]
)
elif
element
==
'l'
:
p1
=
x_co
p2
=
y_co
line_segments
.
append
(
[
int
(
p1
)
,
int
(
p2
)
]
)
fread
.
close
(
)
if
log
is
not
:
log_file
=
open
(
log
,
'a'
)
log_file
.
close
(
)
return

Sort separate line segments in obj format into a continuous polyline or polylines . NOT FINISHED ; DO NOT USE


def measure_topology(fbasename=None, log=None, ml_version=ml_version):
    """Measures mesh topology

    Args:
        fbasename (str): input filename.
        log (str): filename to log output

    Returns:
        dict: dictionary with the following keys:
            vert_num (int): number of vertices
            edge_num (int): number of edges
            face_num (int): number of faces
            unref_vert_num (int): number or unreferenced vertices
            boundry_edge_num (int): number of boundary edges
            part_num (int): number of parts (components) in the mesh.
            manifold (bool): True if mesh is two-manifold, otherwise false.
            non_manifold_edge (int): number of non_manifold edges.
            non_manifold_vert (int): number of non-manifold verices
            genus (int or str): genus of the mesh, either a number or
                'undefined' if the mesh is non-manifold.
            holes (int or str): number of holes in the mesh, either a number
                or 'undefined' if the mesh is non-manifold.

    """
    ml_script1_file = 'TEMP3D_measure_topology.mlx'
    ml_script1 = mlx.FilterScript(file_in=fbasename, ml_version=ml_version)
    compute.measure_topology(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)
    topology = ml_script1.topology
    return topology

def measure_topology ( fbasename = None , log = None , ml_version = ml_version ) : ml_script1_file = 'TEMP3D_measure_topology.mlx' ml_script1 = mlx . FilterScript ( file_in = fbasename , ml_version = ml_version ) compute . measure_topology ( ml_script1 ) ml_script1 . save_to_file ( ml_script1_file ) ml_script1 . run_script ( log = log , script_file = ml_script1_file ) topology = ml_script1 . topology return topology


def measure_all(fbasename=None, log=None, ml_version=ml_version):
    """Measures mesh geometry, aabb and topology."""
    ml_script1_file = 'TEMP3D_measure_gAndT.mlx'
    if ml_version == '1.3.4BETA':
        file_out = 'TEMP3D_aabb.xyz'
    else:
        file_out = None

    ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
    compute.measure_geometry(ml_script1)
    compute.measure_topology(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)
    geometry = ml_script1.geometry
    topology = ml_script1.topology

    if ml_version == '1.3.4BETA':
        if log is not None:
            log_file = open(log, 'a')
            log_file.write(
                '***Axis Aligned Bounding Results for file "%s":\n' %
                fbasename)
            log_file.close()
        aabb = measure_aabb(file_out, log)
    else:
        aabb = geometry['aabb']
    return aabb, geometry, topology

Measures mesh geometry aabb and topology .


def measure_dimension(fbasename=None, log=None, axis1=None, offset1=0.0,
                      axis2=None, offset2=0.0, ml_version=ml_version):
    """Measure a dimension of a mesh"""
    axis1 = axis1.lower()
    axis2 = axis2.lower()
    ml_script1_file = 'TEMP3D_measure_dimension.mlx'
    file_out = 'TEMP3D_measure_dimension.xyz'

    ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
    compute.section(ml_script1, axis1, offset1, surface=True)
    compute.section(ml_script1, axis2, offset2, surface=False)
    layers.delete_lower(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)

    for val in ('x', 'y', 'z'):
        if val not in (axis1, axis2):
            axis = val
    # ord: Get number that represents letter in ASCII
    # Here we find the offset from 'x' to determine the list reference
    # i.e. 0 for x, 1 for y, 2 for z
    axis_num = ord(axis) - ord('x')
    aabb = measure_aabb(file_out, log)
    dimension = {'min': aabb['min'][axis_num], 'max': aabb['max'][axis_num],
                 'length': aabb['size'][axis_num], 'axis': axis}
    if log is None:
        print('\nFor file "%s"' % fbasename)
        print('Dimension parallel to %s with %s=%s & %s=%s:' % (axis, axis1, offset1,
                                                                axis2, offset2))
        print('  Min = %s, Max = %s, Total length = %s' % (dimension['min'],
                                                           dimension['max'], dimension['length']))
    else:
        log_file = open(log, 'a')
        log_file.write('\nFor file "%s"\n' % fbasename)
        log_file.write('Dimension parallel to %s with %s=%s & %s=%s:\n' % (axis, axis1, offset1,
                                                                           axis2, offset2))
        log_file.write('min = %s\n' % dimension['min'])
        log_file.write('max = %s\n' % dimension['max'])
        log_file.write('Total length = %s\n' % dimension['length'])
        log_file.close()
    return dimension


def lowercase_ext(filename):
    """
    This is a helper used by UploadSet.save to provide lowercase extensions for
    all processed files, to compare with configured extensions in the same
    case.

    .. versionchanged:: 0.1.4
       Filenames without extensions are no longer lowercased, only the
       extension is returned in lowercase, if an extension exists.

    :param filename: The filename to ensure has a lowercase extension.
    """
    if '.' in filename:
        main, ext = os.path.splitext(filename)
        return main + ext.lower()
    # For consistency with os.path.splitext,
    # do not treat a filename without an extension as an extension.
    # That is, do not return filename.lower().
    return filename

def lowercase_ext ( filename ) : if '.' in filename : main , ext = os . path . splitext ( filename ) return main + ext . lower ( ) return filename

This is a helper used by UploadSet . save to provide lowercase extensions for all processed files to compare with configured extensions in the same case .


def patch_request_class(app, size=64 * 1024 * 1024):
    """
    By default, Flask will accept uploads to an arbitrary size. While Werkzeug
    switches uploads from memory to a temporary file when they hit 500 KiB,
    it's still possible for someone to overload your disk space with a
    gigantic file.

    This patches the app's request class's
    `~werkzeug.BaseRequest.max_content_length` attribute so that any upload
    larger than the given size is rejected with an HTTP error.

    .. note::

       In Flask 0.6, you can do this by setting the `MAX_CONTENT_LENGTH`
       setting, without patching the request class. To emulate this behavior,
       you can pass `None` as the size (you must pass it explicitly). That is
       the best way to call this function, as it won't break the Flask 0.6
       functionality if it exists.

    .. versionchanged:: 0.1.1

    :param app: The app to patch the request class of.
    :param size: The maximum size to accept, in bytes. The default is 64 MiB.
                 If it is `None`, the app's `MAX_CONTENT_LENGTH` configuration
                 setting will be used to patch.
    """
    if size is None:
        if isinstance(app.request_class.__dict__['max_content_length'],
                      property):
            return
        size = app.config.get('MAX_CONTENT_LENGTH')
    reqclass = app.request_class
    patched = type(reqclass.__name__, (reqclass,),
                   {'max_content_length': size})
    app.request_class = patched

def patch_request_class ( app , size = 64 * 1024 * 1024 ) : if size is None : if isinstance ( app . request_class . __dict__ [ 'max_content_length' ] , property ) : return size = app . config . get ( 'MAX_CONTENT_LENGTH' ) reqclass = app . request_class patched = type ( reqclass . __name__ , ( reqclass , ) , { 'max_content_length' : size } ) app . request_class = patched

By default Flask will accept uploads to an arbitrary size . While Werkzeug switches uploads from memory to a temporary file when they hit 500 KiB it s still possible for someone to overload your disk space with a gigantic file .


def config_for_set(uset, app, defaults=None):
    """
    This is a helper function for `configure_uploads` that extracts the
    configuration for a single set.

    :param uset: The upload set.
    :param app: The app to load the configuration from.
    :param defaults: A dict with keys `url` and `dest` from the
                     `UPLOADS_DEFAULT_DEST` and `DEFAULT_UPLOADS_URL`
                     settings.
    """
    config = app.config
    prefix = 'UPLOADED_%s_' % uset.name.upper()
    using_defaults = False
    if defaults is None:
        defaults = dict(dest=None, url=None)

    allow_extns = tuple(config.get(prefix + 'ALLOW', ()))
    deny_extns = tuple(config.get(prefix + 'DENY', ()))
    destination = config.get(prefix + 'DEST')
    base_url = config.get(prefix + 'URL')

    if destination is None:
        # the upload set's destination wasn't given
        if uset.default_dest:
            # use the "default_dest" callable
            destination = uset.default_dest(app)
        if destination is None: # still
            # use the default dest from the config
            if defaults['dest'] is not None:
                using_defaults = True
                destination = os.path.join(defaults['dest'], uset.name)
            else:
                raise RuntimeError("no destination for set %s" % uset.name)

    if base_url is None and using_defaults and defaults['url']:
        base_url = addslash(defaults['url']) + uset.name + '/'

    return UploadConfiguration(destination, base_url, allow_extns, deny_extns)

def
config_for_set
(
uset
,
app
,
defaults
=
)
:
config
=
app
.
config
prefix
=
'UPLOADED_%s_'
%
uset
.
name
.
upper
(
)
using_defaults
=
False
if
defaults
is
:
defaults
=
dict
(
dest
=
,
url
=
)
allow_extns
=
tuple
(
config
.
get
(
prefix
+
'ALLOW'
,
(
)
)
)
deny_extns
=
tuple
(
config
.
get
(
prefix
+
'DENY'
,
(
)
)
)
destination
=
config
.
get
(
prefix
+
'DEST'
)
base_url
=
config
.
get
(
prefix
+
'URL'
)
if
destination
is
:
if
uset
.
default_dest
:
destination
=
uset
.
default_dest
(
app
)
if
destination
is
:
if
defaults
[
'dest'
]
is
not
:
using_defaults
=
True
destination
=
os
.
path
.
join
(
defaults
[
'dest'
]
,
uset
.
name
)
else
:
raise
RuntimeError
(
"no destination for set %s"
%
uset
.
name
)
if
base_url
is
and
using_defaults
and
defaults
[
'url'
]
:
base_url
=
addslash
(
defaults
[
'url'
]
)
+
uset
.
name
+
'/'
return
UploadConfiguration
(
destination
,
base_url
,
allow_extns
,
deny_extns
)

This is a helper function for configure_uploads that extracts the configuration for a single set .


def configure_uploads(app, upload_sets):
    """
    Call this after the app has been configured. It will go through all the
    upload sets, get their configuration, and store the configuration on the
    app. It will also register the uploads module if it hasn't been set. This
    can be called multiple times with different upload sets.

    .. versionchanged:: 0.1.3
       The uploads module/blueprint will only be registered if it is needed
       to serve the upload sets.

    :param app: The `~flask.Flask` instance to get the configuration from.
    :param upload_sets: The `UploadSet` instances to configure.
    """
    if isinstance(upload_sets, UploadSet):
        upload_sets = (upload_sets,)

    if not hasattr(app, 'upload_set_config'):
        app.upload_set_config = {}
    set_config = app.upload_set_config
    defaults = dict(dest=app.config.get('UPLOADS_DEFAULT_DEST'),
                    url=app.config.get('UPLOADS_DEFAULT_URL'))

    for uset in upload_sets:
        config = config_for_set(uset, app, defaults)
        set_config[uset.name] = config

    should_serve = any(s.base_url is None for s in set_config.values())
    if '_uploads' not in app.blueprints and should_serve:
        app.register_blueprint(uploads_mod)

def configure_uploads ( app , upload_sets ) : if isinstance ( upload_sets , UploadSet ) : upload_sets = ( upload_sets , ) if not hasattr ( app , 'upload_set_config' ) : app . upload_set_config = { } set_config = app . upload_set_config defaults = dict ( dest = app . config . get ( 'UPLOADS_DEFAULT_DEST' ) , url = app . config . get ( 'UPLOADS_DEFAULT_URL' ) ) for uset in upload_sets : config = config_for_set ( uset , app , defaults ) set_config [ uset . name ] = config should_serve = any ( s . base_url is None for s in set_config . values ( ) ) if '_uploads' not in app . blueprints and should_serve : app . register_blueprint ( uploads_mod )

Call this after the app has been configured . It will go through all the upload sets get their configuration and store the configuration on the app . It will also register the uploads module if it hasn t been set . This can be called multiple times with different upload sets .


def config(self):
        """
        This gets the current configuration. By default, it looks up the
        current application and gets the configuration from there. But if you
        don't want to go to the full effort of setting an application, or it's
        otherwise outside of a request context, set the `_config` attribute to
        an `UploadConfiguration` instance, then set it back to `None` when
        you're done.
        """
        if self._config is not None:
            return self._config
        try:
            return current_app.upload_set_config[self.name]
        except AttributeError:
            raise RuntimeError("cannot access configuration outside request")

This gets the current configuration . By default it looks up the current application and gets the configuration from there . But if you don t want to go to the full effort of setting an application or it s otherwise outside of a request context set the _config attribute to an UploadConfiguration instance then set it back to None when you re done .


def url(self, filename):
        """
        This function gets the URL a file uploaded to this set would be
        accessed at. It doesn't check whether said file exists.

        :param filename: The filename to return the URL for.
        """
        base = self.config.base_url
        if base is None:
            return url_for('_uploads.uploaded_file', setname=self.name,
                           filename=filename, _external=True)
        else:
            return base + filename

def url ( self , filename ) : base = self . config . base_url if base is None : return url_for ( '_uploads.uploaded_file' , setname = self . name , filename = filename , _external = True ) else : return base + filename

This function gets the URL a file uploaded to this set would be accessed at . It doesn t check whether said file exists .


def path(self, filename, folder=None):
        """
        This returns the absolute path of a file uploaded to this set. It
        doesn't actually check whether said file exists.

        :param filename: The filename to return the path for.
        :param folder: The subfolder within the upload set previously used
                       to save to.
        """
        if folder is not None:
            target_folder = os.path.join(self.config.destination, folder)
        else:
            target_folder = self.config.destination
        return os.path.join(target_folder, filename)

def path ( self , filename , folder = None ) : if folder is not None : target_folder = os . path . join ( self . config . destination , folder ) else : target_folder = self . config . destination return os . path . join ( target_folder , filename )

This returns the absolute path of a file uploaded to this set . It doesn t actually check whether said file exists .


def extension_allowed(self, ext):
        """
        This determines whether a specific extension is allowed. It is called
        by `file_allowed`, so if you override that but still want to check
        extensions, call back into this.

        :param ext: The extension to check, without the dot.
        """
        return ((ext in self.config.allow) or
                (ext in self.extensions and ext not in self.config.deny))

def extension_allowed ( self , ext ) : return ( ( ext in self . config . allow ) or ( ext in self . extensions and ext not in self . config . deny ) )

This determines whether a specific extension is allowed . It is called by file_allowed so if you override that but still want to check extensions call back into this .


def resolve_conflict(self, target_folder, basename):
        """
        If a file with the selected name already exists in the target folder,
        this method is called to resolve the conflict. It should return a new
        basename for the file.

        The default implementation splits the name and extension and adds a
        suffix to the name consisting of an underscore and a number, and tries
        that until it finds one that doesn't exist.

        :param target_folder: The absolute path to the target.
        :param basename: The file's original basename.
        """
        name, ext = os.path.splitext(basename)
        count = 0
        while True:
            count = count + 1
            newname = '%s_%d%s' % (name, count, ext)
            if not os.path.exists(os.path.join(target_folder, newname)):
                return newname

def resolve_conflict ( self , target_folder , basename ) : name , ext = os . path . splitext ( basename ) count = 0 while True : count = count + 1 newname = '%s_%d%s' % ( name , count , ext ) if not os . path . exists ( os . path . join ( target_folder , newname ) ) : return newname

If a file with the selected name already exists in the target folder this method is called to resolve the conflict . It should return a new basename for the file .